from PySide6.QtWidgets import QApplication, QFrame, QTabWidget, QScrollBar, QInputDialog, QMainWindow, QWidget, QMessageBox, QSpinBox, QPushButton, QGroupBox, QFormLayout, QVBoxLayout, QGridLayout, QTextEdit, QLabel, QLineEdit, QHBoxLayout, QProgressBar, QSizePolicy, QComboBox, QDialog, QGraphicsDropShadowEffect, QScrollArea, QProgressDialog from PySide6.QtCore import Qt, Signal, Slot, QRect, QSettings, QTimer, QThreadPool, QSignalBlocker from PySide6.QtGui import QGuiApplication, QFont, QIcon, QPixmap, QTextOption, QColor from typing import Dict, Any, List, Optional, Tuple from functools import partial from toggleSwitch import ToggleSwitch from browser_control import BrowserController # from whale_translator import WhaleTranslator # from vertexAI import VertexAITranslator from locatorManager_by_SP import LocatorManager from src.cmdDiag.cmb_diag import CMBSettingsDialog from src.cmdDiag.cmb_DB_Manager import CMB_Database_Manager from src.priceSetDiag.priceSettingManager import PriceSettingManager from src.modules.settings_manager import SettingsManager from src.inputDiag.detail_Input_Diag import DetailTextEditor from src.img_module.image_processor_manager import ImageProcessorManager from src.img_module.image_processor_dialog import ImageProcessorDialog from src.limited_contents.bizDBManager import BizDBManager import logging import psutil import sys import json import os, shutil, time import asyncio from datetime import datetime # from src.keyword_manager import KeywordManager # from src.kwDBManager_with_sp import KeywordDBManager # from src.kiprisAPI import Kipris_API from src.keyword.db_manager import DBManager from src.keyword.keyword_manager import KeywordManager from src.keyword.Kipris_for_web import MarkInfoScraper from src.discord_manager import DiscordManager # from src.sp_manager import SupabaseManager from user_manual_dialog import UserManualDialog import configparser from src.unwantedDiag.unwanted_words_dialog import UnwantedWordsDialog from updateManager.__version__ import __gui_log_level__ from src.logDialog.log_dialog import LogDialog from password_change_dialog import PasswordChangeDialog from src.gpuDiag.gpu_status_dialog import GPUStatusDialog from src.modules.gpu_utils import GPUManager import sys # 파일 상단의 다른 임포트와 함께 추가 import os import wmi import platform import subprocess import cv2 import numpy as np from PIL import Image, ImageDraw, ImageFont import threading from src.modules.fonts.fontSelectDialog import FontSelectionDialog class MAIN_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.initial_setting = False self.system_info = system_info or {} self.settings_manager = settings_manager self.version = version self.base_dir = self.get_base_dir() # 사용자 정보 저장 (user_id 등) self.user_info = user_info self.user_membership_level = user_info.get('membership_level', 'basic') self.logger.log(f"사용자 멤버십 레벨 초기화: {self.user_membership_level}", level=logging.INFO) self.authenticated_by_admin = user_info.get("authenticated_by_admin") self.sp_user_id = user_info.get("id") # 예를 들어, 'users' 테이블의 PK 값 self.image_processor = None # 디스코드 관리자 초기화 self.discord_manager = DiscordManager(self.logger) self.job_start_time = None self.supabase_manager = supabase_manager # 이미지 프로세서 관리자 초기화 self.logger.log(f"MAIN_GUI에서 supabase_manager 전달 여부: {self.supabase_manager is not None}", level=logging.INFO) self.image_processor_manager = ImageProcessorManager( logger=self.logger, base_dir=str(self.base_dir) if hasattr(self, 'base_dir') else '.', # 기본 디렉토리 toggle_states=getattr(self, 'toggle_states', {}), # 토글 상태 (초기화 후 사용) unwanted_words=getattr(self, 'unwanted_words', {}), # 불용어 (초기화 후 사용) authenticated_by_admin=self.authenticated_by_admin, image_worker_fatal=lambda: self.logger.log("이미지 워커 치명적 오류 발생", level=logging.CRITICAL), # 예시 콜백 supabase_manager=self.supabase_manager # Supabase 매니저 추가 ) # 이미지 프로세서 다이얼로그 초기화 (미리 생성하여 자동 초기화에 사용) self.image_processor_dialog = ImageProcessorDialog( parent=self, logger=self.logger, supabase_manager=self.supabase_manager, main_window=self, settings_manager=self.settings_manager ) # 워커 상태 메시지 시그널 연결 self.image_processor_dialog.worker_status_message.connect(self.on_worker_status_message) self.image_processor_dialog.worker_health_status.connect(self.on_worker_health_status) # 다이얼로그는 숨김 상태로 유지 (초기화만 위해 사용) self.image_processor_auto_initialized = False # 자동 초기화 플래그 # 토글 위젯을 저장할 딕셔너리 초기화 self.toggle_widgets = {} # 수동 로그인 QMessageBox 참조 저장 self.manual_login_message_box = None # 업로드조건 설정 변수들 초기화 (settings에서 로드) self.upload_price_policy = self.settings_manager.get_value("upload_conditions/price_policy", 1) # 1: 최대 업로드 갯수 기준, 2: 최저 옵션가 기준, 3: 상품별 각각의 대표가격 설정 self.smartstore_market_policy = self.settings_manager.get_value("upload_conditions/smartstore_market_policy", True) # 스스 마켓정책적용 # 로드된 업로드조건 설정값 로그 출력 if hasattr(self, 'logger') and self.logger: self.logger.log(f"업로드조건 초기값 로드 - 가격정책: {self.upload_price_policy}, 스스정책: {self.smartstore_market_policy}", level=logging.DEBUG) # 자동 스크롤 관련 변수 초기화 self.scroll_timer = QTimer(self) self.scroll_timer.setInterval(500) # 0.5초 간격 self.scroll_timer.timeout.connect(self._do_scroll_step) self.current_cycle = 0 self.total_cycles = 3 val = self.settings_manager.get_value("global/collect_method") self.logger.log(f"[시작직후] settings_manager에서 읽은 global/collect_method: {val}", level=logging.DEBUG) val = self.settings_manager.get_value("collect_method_combo") self.logger.log(f"[시작직후] settings_manager에서 읽은 collect_method_combo: {val}", level=logging.DEBUG) self.setup_widget_maps() # GPU 상태 확인 (번역 엔진 드롭다운 구성용) - initUI 이후에 초기화 self.gpu_manager_for_ui = None # CUDA 사용 가능한 환경에서 migan_use_cuda 강제 활성화 if hasattr(self, 'toggle_states') and 'migan_use_cuda' in self.toggle_states: self.toggle_states['migan_use_cuda'] = True self.logger.log("🎯 migan_use_cuda를 True로 강제 설정", level=logging.INFO) # 메모리 모니터링 초기화 (UI 생성 전에 실행) self.init_memory_monitoring() self.initUI() # 모던 테마 적용 self.apply_modern_theme() # GPU 가용성 확인 및 드롭다운 폴백 처리 (UI 초기화 완료 후) QTimer.singleShot(100, self.validate_all_translation_engines) # 설정 관리자 초기화 self.load_settings() self.initialize_user_session() self.logger.log(f"로그기록이 설정되었습니다.", level=logging.INFO) # update_log 인자가 있다면 표시 if update_log: QMessageBox.information(self, "업데이트 변경사항", update_log) self.debug = False # 토글 위젯을 저장할 딕셔너리 초기화 self.toggle_widgets = {} self.locator_manager = LocatorManager(self.supabase_manager, self.logger) # self.vertexAI = VertexAITranslator(self.logger) self.request_inpainting_server_url = self.locator_manager.get_locator('Global', 'request_inpainting_server_url') self.request_rembg_server_url = self.locator_manager.get_locator('Global', 'request_rembg_server_url') self.request_rembg_server_url_local = self.locator_manager.get_locator('Global', 'request_rembg_server_url_local') self.gemma_api_base_url = self.locator_manager.get_locator('Global', 'gemma_api_base_url') # 개발환경용 인페인팅 서버 URL도 가져오기 (없으면 None) self.request_inpainting_server_url_local = self.locator_manager.get_locator('Global', 'request_inpainting_server_url_local') self.image_worker_restart_every = int(self.locator_manager.get_locator('Global', 'image_worker_restart_every')) self.products_per_context_restart = int(self.locator_manager.get_locator('Global', 'products_per_context_restart')) self.gemma_api_base_url_local = self.locator_manager.get_locator('Global', 'gemma_api_base_url_local') self.toggle_states['request_inpainting_server_url'] = self.request_inpainting_server_url self.toggle_states['request_rembg_server_url'] = self.request_rembg_server_url self.toggle_states['request_rembg_server_url_local'] = self.request_rembg_server_url_local self.toggle_states['gemma_api_base_url'] = self.gemma_api_base_url # 개발환경용 인페인팅 서버 URL도 추가 self.toggle_states['request_inpainting_server_url_local'] = self.request_inpainting_server_url_local self.toggle_states['image_worker_restart_every'] = self.image_worker_restart_every self.toggle_states['products_per_context_restart'] = self.products_per_context_restart self.toggle_states['gemma_api_base_url_local'] = self.gemma_api_base_url_local self.logger.log(f"request_inpainting_server_url 업데이트: {self.request_inpainting_server_url}", level=logging.DEBUG) self.logger.log(f"request_inpainting_server_url_local 업데이트: {self.request_inpainting_server_url_local}", level=logging.DEBUG) self.logger.log(f"request_rembg_server_url 업데이트: {self.request_rembg_server_url}", level=logging.DEBUG) self.logger.log(f"request_rembg_server_url_local 업데이트: {self.request_rembg_server_url_local}", level=logging.DEBUG) self.logger.log(f"image_worker_restart_every 업데이트: {self.image_worker_restart_every}", level=logging.DEBUG) self.logger.log(f"products_per_context_restart 업데이트: {self.products_per_context_restart}", level=logging.DEBUG) self.logger.log(f"gemma_api_base_url 업데이트: {self.gemma_api_base_url}", level=logging.DEBUG) self.logger.log(f"gemma_api_base_url_local 업데이트: {self.gemma_api_base_url_local}", level=logging.DEBUG) self.toggle_states['membership_level'] = self.user_membership_level self.logger.log(f"membership_level 업데이트 : {self.toggle_states['membership_level']}", level=logging.DEBUG) # ONNX 모델 타입 기본값 설정 self.toggle_states['onnx_model_type'] = self.settings_manager.get_value("option/onnx_model_type", "자동 선택") self.logger.log(f"ONNX 모델 타입 설정: {self.toggle_states['onnx_model_type']}", level=logging.DEBUG) # GPU 정보를 toggle_states에 추가 (ONNX 모듈에서 사용) try: gpu_info = self.get_gpu_info() self.toggle_states['gpu_info'] = { 'has_directx12': gpu_info['has_directx12'], 'gpu_type': gpu_info['gpu_type'], # 'integrated' or 'dedicated' 'vendor': gpu_info['vendor'], # 'intel', 'amd', 'nvidia' 'recommended_model': gpu_info['recommended_model'] # 'fp16', 'opt', 'simp' } self.logger.log(f"GPU 정보 toggle_states 등록: {self.toggle_states['gpu_info']}", level=logging.DEBUG) except Exception as e: # GPU 정보 가져오기 실패 시 기본값 설정 self.toggle_states['gpu_info'] = { 'has_directx12': False, 'gpu_type': 'unknown', 'vendor': 'unknown', 'recommended_model': 'simp' } self.logger.log(f"GPU 정보 가져오기 실패, 기본값 설정: {e}", level=logging.WARNING) # DB 파일 경로 설정 self.toggle_states['base_dir'] = self.base_dir # 내장 rembg 모델 경로 설정 self.toggle_states['local_rembg_model_path'] = os.path.join(self.base_dir, "modules", "rembg_models", "birefnet-general-lite.onnx") self.settings_manager.save_value("local_rembg_model_path", self.toggle_states['local_rembg_model_path']) self.logger.log(f"local_rembg_model_path 경로: {self.toggle_states['local_rembg_model_path']}", level=logging.DEBUG) # MIGAN ONNX 경로 설정 # self.toggle_states['migan_onnx_path'] = os.path.join(self.base_dir, "modules", "migan_onnx", "migan_pipeline_v2.onnx") self.toggle_states['migan_onnx_path'] = os.path.join(self.base_dir, "modules", "migan_onnx", "migan_pipeline_v2_simplified.onnx") # # 폰트 경로 설정 # self.toggle_states['image_font_path'] = os.path.join(self.base_dir, "fonts", "HakgyoansimDunggeunmisoTTFB.ttf") # self.toggle_states['watermark_font_path'] = os.path.join(self.base_dir, 'fonts', 'HakgyoansimDunggeunmisoTTFB.ttf') saved_image_font_path = self.settings_manager.get_value("image_font_path") self.toggle_states['image_font_path'] = saved_image_font_path if saved_image_font_path else os.path.join(self.base_dir, "modules", "fonts", "HakgyoansimDunggeunmisoTTFB.ttf") saved_watermark_font_path = self.settings_manager.get_value("watermark_font_path") self.toggle_states['watermark_font_path'] = saved_watermark_font_path if saved_watermark_font_path else os.path.join(self.base_dir, "modules", "fonts", "HakgyoansimDunggeunmisoTTFB.ttf") self.logger.log(f"[시작직후] settings_manager에서 읽은 image_font_path: {saved_image_font_path}", level=logging.DEBUG) self.logger.log(f"[시작직후] settings_manager에서 읽은 watermark_font_path: {saved_watermark_font_path}", level=logging.DEBUG) # 임시 이미지 폴더 (작업 중) self.toggle_states['TEMP_IMAGE_DIR'] = os.path.join(self.base_dir, 'temp_images') os.makedirs(self.toggle_states['TEMP_IMAGE_DIR'], exist_ok=True) self.logger.log(f"임시 디렉토리 생성: {self.toggle_states['TEMP_IMAGE_DIR']}", level=logging.INFO) # 2. 에러 스크린샷 폴더 (장기 보관) self.toggle_states['ERROR_SCREENSHOT_DIR'] = os.path.join(self.base_dir, 'error_screenshots') os.makedirs(self.toggle_states['ERROR_SCREENSHOT_DIR'], exist_ok=True) self.logger.log(f"error 디렉토리 생성: {self.toggle_states['ERROR_SCREENSHOT_DIR']}", level=logging.INFO) 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.detail_text_db_path = os.path.join(self.base_db_dir, "detail_text.db") self.price_db_path = os.path.join(self.base_db_dir, "price_settings.db") self.kw_db_path = os.path.join(self.base_db_dir, f"user_data_{self.sp_user_id}.db") self.biz_db_path = os.path.join(self.base_db_dir, f"biz_data_{self.sp_user_id}.db") self.logger.log(f"detail_text_db_path 경로: {self.detail_text_db_path}", level=logging.DEBUG) self.logger.log(f"price_db_path 경로: {self.price_db_path}", level=logging.DEBUG) self.logger.log(f"kw_db_path 경로: {self.kw_db_path}", level=logging.DEBUG) self.logger.log(f"biz_db_path 경로: {self.biz_db_path}", level=logging.DEBUG) # BizDBManager 초기화 try: self.biz_dbManager = BizDBManager(self.biz_db_path) except Exception as e: self.logger.log(f"BizDBManager 초기화 실패: {e}", level=logging.ERROR) self.biz_dbManager = None if self.initial_setting: self.logger.log(f"초기 설정 파일 생성 중...", level=logging.INFO) self.thread_pool = QThreadPool() # 스레드 풀 초기화 self.kwdb_manager = DBManager(self.kw_db_path, logger=self.logger, user_id=self.sp_user_id, spManager=self.supabase_manager) self.kiprisapi = MarkInfoScraper(logger=self.logger) self.keyword_manager = KeywordManager(logger=self.logger, db_manager=self.kwdb_manager, sp_manager=self.supabase_manager, kipris_api=self.kiprisapi, user_info=self.user_info, thread_pool=self.thread_pool, parent=self) self.detail_text_widget = DetailTextEditor(logger=logger, sp_manager=self.supabase_manager, user_id=self.sp_user_id) # kwdb_manager를 그대로 사용 (price_db_manager는 더 이상 필요 없음) self.price_setting_diag = PriceSettingManager(parent=self, logger=self.logger, user_id=self.sp_user_id, db_path=self.kw_db_path, db_manager=self.kwdb_manager, debug=self.debug) self.browser_controller = BrowserController(self, self.logger, self.locator_manager, self.price_setting_diag, self.detail_text_widget, self.login_infos, self.toggle_states, user_id=self.sp_user_id, supabase_manager=self.supabase_manager) # BizDBManager 주입 self.browser_controller.biz_dbManager = self.biz_dbManager # 이미지 프로세서 매니저 주입 (details.py 등에서 재시작 제어를 위해 필요) self.browser_controller.image_processor_manager = self.image_processor_manager # 브라우저 시작 완료 및 오류 시그널 연결 self.browser_controller.browser_started.connect(self.on_browser_started) self.browser_controller.unknown_browser_error.connect(self.on_unknown_browser_error) self.browser_controller.browser_create_error.connect(self.on_browser_create_error) self.browser_controller.image_processor_error.connect(self.on_image_processor_error) self.browser_controller.image_worker_fatal_signal.connect(self.on_image_worker_fatal) self.browser_controller.premium_event_started.connect(self.on_premium_event_started) self.browser_controller.browser_login_error.connect(self.on_browser_login_error) # 업로드 관련 시그널 연결 (항상 연결되도록) if hasattr(self.browser_controller, 'upload_progress_updated'): self.browser_controller.upload_progress_updated.connect(self.on_upload_progress_updated) if hasattr(self.browser_controller, 'upload_step_changed'): self.browser_controller.upload_step_changed.connect(self.on_upload_step_changed) if hasattr(self.browser_controller, 'upload_completed'): self.browser_controller.upload_completed.connect(self.on_upload_completed) if hasattr(self.browser_controller, 'upload_log_message'): self.browser_controller.upload_log_message.connect(self.on_upload_log_message) if hasattr(self.browser_controller, 'step_completed'): self.browser_controller.step_completed.connect(self.on_step_completed) self.browser_controller.browser_ad1_close_error.connect(self.on_browser_ad1_close_error) self.browser_controller.browser_ad2_close_error.connect(self.on_browser_ad2_close_error) self.browser_controller.browser_group_list_error.connect(self.on_browser_group_list_error) self.browser_controller.browser_handler_update_error.connect(self.on_browser_handler_update_error) self.browser_controller.browser_parsing_page_error.connect(self.on_browser_parsing_page_error) self.browser_controller.browser_registered_product_page_error.connect(self.on_browser_registered_product_page_error) self.browser_controller.browser_new_product_page_error.connect(self.on_browser_new_product_page_error) self.browser_controller.complete_remove_market_info_job.connect(self.on_complete_remove_market_info_job) # 마켓정보 변경 완료 시그널 연결 self.browser_controller.market_change_completed.connect(self.on_market_change_completed) # 수동 로그인 요청 시그널 연결 self.browser_controller.manual_login_required.connect(self.on_manual_login_required) # 로그인 완료 시그널 연결 (QMessageBox 자동 닫기용) self.browser_controller.login_completed_signal.connect(self.on_login_completed) # # 캡차 발생 시그널 연결 # self.browser_controller.whale_translator.captcha_detected.connect(self.on_captcha_detected) # 상품수정관련련 시그널 연결 self.browser_controller.translation_started.connect(self.on_PercentyJob_started) self.browser_controller.translation_completed.connect(self.on_PercentyJob_completed) self.browser_controller.translation_error.connect(self.on_PercentyJob_error) # 현황 표시 시그널 연결 self.browser_controller.total_progressbar_signal.connect(self.update_total_progress) # 상품수정 단계표시 시그널 연결 self.browser_controller.start_stage_signal.connect(self.start_stage) self.browser_controller.complete_stage_signal.connect(self.complete_stage) self.browser_controller.update_detail_progress_signal.connect(self.update_detail_progress_value) self.browser_controller.set_progress_visible_signal.connect(self.set_progress_visibility) self.browser_controller.percentyJob_button_Enable.connect(self.percentyJob_button_Enable) # 브라우저 컨트롤러의 선택된 그룹 이름 시그널 연결 self.browser_controller.selected_group_name_signal.connect(self.update_selected_group_label) self.browser_controller.group_list_signal.connect(self.update_group_list) # self.config = self.load_config("config.ini") # self.kipris_api_key = self.config.get("Kipris_API", "api_key") # 1kw_db_path = os.path.join("src", "ForbiddenKeyword.db") # self.spManager = SupabaseManager().get_client() # KeywordDBManager에 user_id 전달 (로그인한 사용자별 DB 동기화를 위해) # self.kw_db_manager = KeywordDBManager(kw_db_path, self.user_id, supabase_manager, self.logger) # self.kiprisapi = Kipris_API(logger=self.logger, apikey=self.kipris_api_key) # self.keyword_manager = KeywordManager(logger=self.logger, kw_db_manager=self.kw_db_manager, kipris_api=self.kiprisapi, user_info=self.user_info, parent=self) # self.clipboardImageManager = ClipboardImageManager(self, logger, self.browser_controller, watermark_font_size=36, debug=self.debug) # self.optionHandler = OptionHandler(self.locator_manager, self.browser_controller, self.whale_translator, self.clipboardImageManager, self.logger, self.vertexAI, self.debug) # self.priceHandler = PriceHandler(self.locator_manager, self.browser_controller, self.logger, self.optionHandler, self.vertexAI, self.cmb_diag, self.debug) # self.titleHandler = TitleHandler(self.locator_manager, self.browser_controller, self.logger) self.running = False # 변수 설정 self.start_time = 0 self.finish_time = 0 self.total_product_count = 0 self.current_product_count = 0 self.title_count = 0 self.option_count = 0 self.price_count = 0 self.detail_image_count = 0 self.thumb_image_count = 0 self.current_options_info = {} self.current_stage_index = 0 # 현재 진행 중인 단계 인덱스 # self.start_stage_signal.connect(self.start_stage) # self.complete_stage_signal.connect(self.complete_stage) # self.update_detail_progress_signal.connect(self.update_detail_progress_value) # 연결 # self.set_progress_visible_signal.connect(self.set_progress_visibility) # self.percentyJob_button_Signal.connect(self.percentyJob_button_Enable) self.kill_autohotkey_process() # UI 초기화 완료 후 사업자관리 버튼 라벨 업데이트 (지연 실행) QTimer.singleShot(100, self.update_biz_manage_button_label) def get_state_key(self, widget_name): config = self.widget_map.get(widget_name, {}) return config.get("state_key", widget_name) def setup_widget_maps(self): # 위젯 <-> settings key 매핑 self.widget_map = { # 관리자 "admin_toggle": { "key": "admin/admin_toggle", # 관리자 여부 토글 "state_key": "is_admin", "dependents": { "on": [], "off": [] }, "visible": { # ON/OFF별 visible 위젯 리스트 "on": ["admin_pw_input", "admin_pw_label"], "off": ["user_id_input", "user_pw_input", "user_id_label", "user_pw_label"], } }, "admin_id_input": { "key": "admin/admin_id_input", # 관리자 ID 입력 "state_key": "admin_id", "dependents": { "on": [], "off": [] }, "visible": { # ON/OFF별 visible 위젯 리스트 "on": [], "off": [], } }, "admin_pw_input": { "key": "admin/admin_pw_input", # 관리자 PW 입력 "state_key": "admin_pw", "dependents": { "on": [], "off": [] }, "visible": { # ON/OFF별 visible 위젯 리스트 "on": [], "off": [], } }, "admin_pw_label": { "key": "admin/admin_pw_label", # 관리자 PW 입력 라벨 "state_key": "admin_pw", "dependents": { "on": [], "off": [] }, "visible": { # ON/OFF별 visible 위젯 리스트 "on": [], "off": [], } }, "user_id_input": { "key": "admin/user_id_input", # 직원 ID 입력 "state_key": "user_id", "dependents": { "on": [], "off": [] }, "visible": { # ON/OFF별 visible 위젯 리스트 "on": [], "off": [], } }, "user_pw_input": { "key": "admin/user_pw_input", # 직원 PW 입력 "state_key": "user_pw", "dependents": { "on": [], "off": [] }, "visible": { # ON/OFF별 visible 위젯 리스트 "on": [], "off": [], } }, # 글로벌 "collect_method_combo": { "key": "global/collect_method", "type": "data", # 또는 "text" "state_key": "collect_method_combo", "dependents": { "api": [], "lens": [] }, "visible": { # "api": ["client_id_input", "client_secret_input", "client_id_label", "client_secret_label"], "api": [], "lens": [] } }, # 등록상품 모드 신규 위젯들 "thumb_represent_toggle": { "key": "register/thumb_represent", "state_key": "thumb_represent", "dependents": { "on": [], "off": [] }, "visible": { "on": [], "off": [] } }, "price_range_toggle": { "key": "price/enable_range", "state_key": "price_range_enabled", "dependents": { "on": ["price_range_spin", "price_range_spin_label"], "off": [] }, "visible": { "on": ["price_range_spin", "price_range_spin_label"], "off": [] } }, "price_range_spin": { "key": "price/range_percent", "state_key": "price_range_percent", "dependents": { "on": [], "off": [] }, "visible": { "on": [], "off": [] } }, "option_numbering_shuffle_toggle": { "key": "option/shuffle_names", "state_key": "option_numbering_shuffle", "dependents": { "on": [], "off": [] }, "visible": { "on": [], "off": [] } }, # "client_id_input": { # "key": "global/client_id_text", # 쇼핑검색API 클라이언트ID # "state_key": "clientID", # "dependents": { # ON/OFF별 enable 위젯 리스트 # "on": [], # "off": [] # }, # "visible": { # ON/OFF별 visible 위젯 리스트 # "on": [], # "off": [], # } # }, # "client_secret_input": { # "key": "global/client_secret_text", # 쇼핑검색API 클라이언트 secret # "state_key": "clientSecret", # "dependents": { # ON/OFF별 enable 위젯 리스트 # "on": [], # "off": [] # }, # "visible": { # ON/OFF별 visible 위젯 리스트 # "on": [], # "off": [], # } # }, # "client_id_label": { # "key": "global/client_id_text_label", # 쇼핑검색API 클라이언트ID 라벨 # "dependents": { # ON/OFF별 enable 위젯 리스트 # "on": [], # "off": [] # }, # "visible": { # ON/OFF별 visible 위젯 리스트 # "on": [], # "off": [], # } # }, # "client_secret_label": { # "key": "global/client_secret_text_label", # 쇼핑검색API 클라이언트 secret 라벨 # "dependents": { # ON/OFF별 enable 위젯 리스트 # "on": [], # "off": [] # }, # "visible": { # ON/OFF별 visible 위젯 리스트 # "on": [], # "off": [], # } # }, "memo_toggle": { "key": "global/memo_enabled", # 메모 기능 사용 여부 "state_key": "memo", "dependents": { # ON/OFF별 enable 위젯 리스트 "on": [], "off": [] }, "visible": { # ON/OFF별 visible 위젯 리스트 "on": ["memo_toggle_order_label", "memo_toggle_order", "memo_toggle_exposer_label", "memo_toggle_exposer"], "off": [], } }, "memo_toggle_order_label": { "key": "global/memo_toggle_order_label", # 메모 순서 라벨 "state_key": "memo_toggle_order", "dependents": { # ON/OFF별 enable 위젯 리스트 "on": [], "off": [] } }, "memo_toggle_order": { "key": "global/memo_toggle_order", # 메모 순서 토글 "state_key": "memo_toggle_order", "dependents": { # ON/OFF별 enable 위젯 리스트 "on": [], "off": [] } }, "memo_toggle_exposer_label": { "key": "global/memo_toggle_exposer_label", # 메모 노출 라벨 "state_key": "memo_toggle_exposer", "dependents": { # ON/OFF별 enable 위젯 리스트 "on": [], "off": [] } }, "memo_toggle_exposer": { "key": "global/memo_toggle_exposer", # 메모 노출 토글 "state_key": "memo_toggle_exposer", "dependents": { # ON/OFF별 enable 위젯 리스트 "on": [], "off": [] } }, "ocr_toggle": { "key": "global/ocr_enabled", # OCR 기능 사용 여부 "state_key": "ocr", "dependents": { # ON/OFF별 enable 위젯 리스트 "on": ["unwanted_words_button"], "off": [] }, "visible": { # ON/OFF별 visible 위젯 리스트 "on": ["unwanted_words_button_label", "unwanted_words_button"], "off": [], } }, "unwanted_words_button": { "key": "global/unwanted_words_button_enabled", # OCR 토글에 따른 버튼 사용 여부 "dependents": { # ON/OFF별 enable 위젯 리스트 "on": [], "off": [] }, "visible": { # ON/OFF별 visible 위젯 리스트 "on": [], "off": [], } }, "unwanted_words_button_label": { "key": "global/unwanted_words_button_label", # OCR 토글에 따른 버튼 사용 여부 "dependents": { # ON/OFF별 enable 위젯 리스트 "on": [], "off": [] }, "visible": { # ON/OFF별 visible 위젯 리스트 "on": [], "off": [], } }, "interval_spinbox_label": { "key": "global/interval_spinbox_label", # 시간간격 라벨벨 "dependents": { # ON/OFF별 enable 위젯 리스트 "on": [], "off": [] }, "visible": { # ON/OFF별 visible 위젯 리스트 "on": [], "off": [], } }, "interval_spinbox": { "key": "global/interval_spinbox", # 시간간격 "state_key": "interval", "dependents": { # ON/OFF별 enable 위젯 리스트 "on": [], "off": [] }, "visible": { # ON/OFF별 visible 위젯 리스트 "on": [], "off": [], } }, "watingTime_spinbox_label": { "key": "global/watingTime_spinbox_label", # 번역대기시간 라벨벨 "dependents": { # ON/OFF별 enable 위젯 리스트 "on": [], "off": [] }, "visible": { # ON/OFF별 visible 위젯 리스트 "on": [], "off": [], } }, "watingTime_spinbox": { "key": "global/watingTime_spinbox", # 번역대기시간 "state_key": "watingTime", "dependents": { # ON/OFF별 enable 위젯 리스트 "on": [], "off": [] }, "visible": { # ON/OFF별 visible 위젯 리스트 "on": [], "off": [], } }, "font_type_combo": { "key": "global/font_type_combo", # 폰트 타입 "state_key": "font_type", "dependents": { # ON/OFF별 enable 위젯 리스트 "on": [], "off": [] }, "visible": { # ON/OFF별 visible 위젯 리스트 "on": [], "off": [], } }, "debug_toggle": { "key": "global/debug_enabled", # 디버그 사용 여부 "state_key": "debug_mode", "dependents": { # ON/OFF별 enable 위젯 리스트 "on": [], "off": [] }, "visible": { # ON/OFF별 visible 위젯 리스트 "on": [], "off": [], } }, # 상품명 "title_toggle": { "key": "title/title_enabled", # 상품명 AI 수정 사용 여부 "state_key": "title", "dependents": { # ON/OFF별 enable 위젯 리스트 "on": [], "off": [] }, "visible": { # ON/OFF별 visible 위젯 리스트 "on": [ "title_trans_type_toggle_label", "title_trans_type_toggle", "forbidden_match_title_toggle_label", "forbidden_match_title_toggle" ], "off": [], } }, "title_trans_type_toggle": { "key": "title/title_trans_type_enabled", # 상품명 수정을 위한 번역엔진 설정 "state_key": "title_trans_type", "dependents": { # ON/OFF별 enable 위젯 리스트 "on": [], "off": [] }, "visible": { # ON/OFF별 visible 위젯 리스트 "on": [], "off": [], } }, "title_trans_type_toggle_label": { "key": "title/title_trans_type_label", # 상품명 수정을 위한 번역엔진 라벨 "dependents": { # ON/OFF별 enable 위젯 리스트 "on": [], "off": [] }, "visible": { # ON/OFF별 visible 위젯 리스트 "on": [], "off": [], } }, "cat_rec_toggle": { "key": "title/cat_rec_enabled", # 카테고리 추천버튼 클릭 여부 "state_key": "cat_rec", "dependents": { # ON/OFF별 enable 위젯 리스트 "on": [], "off": [] }, "visible": { # ON/OFF별 visible 위젯 리스트 "on": [], "off": [], } }, "title_shuffle_toggle": { "key": "title/title_shuffle_enabled", # 카테고리 추천버튼 클릭 여부 "state_key": "title_shuffle", "dependents": { # ON/OFF별 enable 위젯 리스트 "on": [], "off": [] }, "visible": { # ON/OFF별 visible 위젯 리스트 "on": [], "off": [], } }, "keyword_fix_toggle": { "key": "title/keyword_fix_toggle", # 키워드 고정 여부 "state_key": "fixed_keywords", "dependents": { # ON/OFF별 enable 위젯 리스트 "on": ["keyword_fix_count_input"], "off": [] }, "visible": { # ON/OFF별 visible 위젯 리스트 "on": [], "off": [], } }, "keyword_fix_count_input": { "key": "title/keyword_fix_count_input_enabled", # 키워드 고정시 고정숫자 설정 "state_key": "fixed_keywords_count", "dependents": { # ON/OFF별 enable 위젯 리스트 "on": [], "off": [] }, "visible": { # ON/OFF별 visible 위젯 리스트 "on": [], "off": [], } }, "sub_word_remove_toggle": { "key": "title/sub_word_remove_toggle", "state_key": "sub_word_remove", "dependents": { "on": [], "off": [] }, "visible": { "on": [], "off": [] } }, "del_warning_word_toggle": { "key": "title/del_warning_word_toggle", "state_key": "del_warning_word", "dependents": { "on": [], "off": [] }, "visible": { "on": [], "off": [] } }, "forbidden_match_title_toggle_label": { "key": "title/forbidden_match_title_label", "dependents": { "on": [], "off": [] }, "visible": { "on": [], "off": [] } }, "forbidden_match_title_toggle": { "key": "title/forbidden_match_title_toggle", "state_key": "forbidden_partial_title", "dependents": { "on": [], "off": [] }, "visible": { "on": [], "off": [] } }, "title_length_limit_input": { "key": "title/title_length_limit_input_enabled", # 상품명 최대 길이 설정 "state_key": "title_length_limit", "dependents": { # ON/OFF별 enable 위젯 리스트 "on": [], "off": [] }, "visible": { # ON/OFF별 visible 위젯 리스트 "on": [], "off": [], } }, # 옵션 "optionTrnas_toggle": { "key": "option/option_trans_enabled", # 옵션 번역 사용 여부 "state_key": "optionTrnas", "dependents": { # ON/OFF별 enable 위젯 리스트 "on": [], "off": [] }, "visible": { # ON/OFF별 visible 위젯 리스트 "on": [ 'optionTrnas_method_toggle', 'optionTrnas_method_label', 'forbidden_match_option_toggle_label', 'forbidden_match_option_toggle', 'optionNumbering_method_combo', 'optionNumbering_method_label' ], "off": [], } }, "optionTrnas_method_toggle": { "key": "option/option_trans_method_enabled", # 옵션 번역 사용 여부 "state_key": "optionTrnas_method", "dependents": { # ON/OFF별 enable 위젯 리스트 "on": [], "off": [] }, "visible": { # ON/OFF별 visible 위젯 리스트 "on": [], "off": [], } }, "optionTrnas_method_label": { "key": "option/option_trans_method_label", # 옵션 번역 사용 여부 "state_key": "", "dependents": { # ON/OFF별 enable 위젯 리스트 "on": [], "off": [] }, "visible": { # ON/OFF별 visible 위젯 리스트 "on": [], "off": [], } }, "optionNumbering_method_combo": { "key": "option/option_numbering_method_type", # 옵션 번호 매기기 방식 "state_key": "optionNumbering_method_type", "dependents": { # ON/OFF별 enable 위젯 리스트 "on": [], "off": [] }, "visible": { # ON/OFF별 visible 위젯 리스트 "on": [], "off": [], } }, "optionNumbering_method_label": { "key": "option/option_numbering_method_label", # 옵션 번호 사용 여부 "dependents": { # ON/OFF별 enable 위젯 리스트 "on": [], "off": [] }, "visible": { # ON/OFF별 visible 위젯 리스트 "on": [], "off": [], } }, "optionIMGTrans_toggle": { "key": "option/option_img_trans_enabled", # 옵션 이미지 번역 사용 여부 "state_key": "optionIMGTrans", "dependents": { # ON/OFF별 enable 위젯 리스트 "on": [], "off": [] }, "visible": { # ON/OFF별 visible 위젯 리스트 "on": [], "off": [], } }, "optionIMGTrans_type_label": { "key": "option/optionIMGTrans_type_label", # 옵션 이미지 번역 타입 라벨 "dependents": { # ON/OFF별 enable 위젯 리스트 "on": [], "off": [] }, "visible": { # ON/OFF별 visible 위젯 리스트 "on": [], "off": [], } }, "forbidden_match_option_toggle_label": { "key": "option/forbidden_match_option_label", "dependents": { "on": [], "off": [] }, "visible": { "on": [], "off": [] } }, "forbidden_match_option_toggle": { "key": "option/forbidden_match_option_toggle", "state_key": "forbidden_partial_option", "dependents": { "on": [], "off": [] }, "visible": { "on": [], "off": [] } }, "onnx_model_type_combo": { "key": "option/onnx_model_type", # ONNX 모델 타입 설정 "state_key": "onnx_model_type", "type": "text", # 텍스트 기반 콤보박스 "dependents": { # ON/OFF별 enable 위젯 리스트 "on": [], "off": [] }, "visible": { # ON/OFF별 visible 위젯 리스트 "on": [], "off": [], } }, "first_option_img_to_thumb_toggle": { "key": "option/first_option_img_to_thumb", # 첫 번째 옵션 썸네일 복사 사용 여부 "state_key": "first_option_img_to_thumb", "dependents": { # ON/OFF별 enable 위젯 리스트 "on": [], "off": [] }, "visible": { # ON/OFF별 visible 위젯 리스트 "on": [], "off": [], } }, "optionAutoSelect_toggle": { "key": "option/option_auto_select_enabled", # 옵션 자동 선택 사용 여부 "state_key": "optionAutoSelect", "dependents": { # ON/OFF별 enable 위젯 리스트 "on": ["max_option_count_input", "optionName_max_length_input"], "off": [] }, "visible": { # ON/OFF별 visible 위젯 리스트 "on": [], "off": [], } }, "max_option_count_input": { "key": "option/max_option_count_input_enabled", # 옵션 최대 개수 설정 "state_key": "max_option_count", "dependents": { # ON/OFF별 enable 위젯 리스트 "on": [], "off": [] }, "visible": { # ON/OFF별 visible 위젯 리스트 "on": [], "off": [], } }, "optionName_max_length_input": { "key": "option/optionName_max_length_input_enabled", # 옵션명 최대 길이 설정 "state_key": "optionName_max_length", "dependents": { # ON/OFF별 enable 위젯 리스트 "on": [], "off": [] }, "visible": { # ON/OFF별 visible 위젯 리스트 "on": [], "off": [], } }, # 태그 "tag_toggle": { "key": "tag/tag_toggle", # 태그 수정 사용 여부 "state_key": "tag", "dependents": { # ON/OFF별 enable 위젯 리스트 "on": [], "off": [] }, "visible": { # ON/OFF별 visible 위젯 리스트 "on": [ "tag_method_widget", "tag_method_label", "tag_method_combo", "delete_all_tags_widget", "delete_all_tags_toggle_label", "delete_all_tags_toggle", "forbidden_match_tag_widget", "forbidden_match_tag_toggle_label", "forbidden_match_tag_toggle" ], "off": [], } }, "tag_method_combo": { "key": "tag/tag_method_combo", # 태그 생성 방식 선택 "state_key": "tag_method", "type": "data", "dependents": { # ON/OFF별 enable 위젯 리스트 "on": [], "off": [] }, "visible": { # ON/OFF별 visible 위젯 리스트 "on": [], "off": [], } }, "delete_all_tags_toggle": { "key": "tag/delete_all_tags_toggle", # 모든 태그 삭제 여부 "state_key": "delete_all_tags", "dependents": { # ON/OFF별 enable 위젯 리스트 "on": [], "off": [] }, "visible": { # ON/OFF별 visible 위젯 리스트 "on": [], "off": [], } }, "forbidden_match_tag_toggle_label": { "key": "tag/forbidden_match_tag_label", "dependents": { "on": [], "off": [] }, "visible": { "on": [], "off": [] } }, "forbidden_match_tag_toggle": { "key": "tag/forbidden_match_tag_toggle", "state_key": "forbidden_partial_tag", "dependents": { "on": [], "off": [] }, "visible": { "on": [], "off": [] } }, # 가격 "price_toggle": { "key": "price/price_toggle", # 가격 수정 사용 여부 "state_key": "price", "dependents": { # ON/OFF별 enable 위젯 리스트 "on": [], "off": [] }, "visible": { # ON/OFF별 visible 위젯 리스트 "on": ["cmb_button"], "off": [], } }, "remove_overprice_toggle": { "key": "price/remove_overprice_toggle", # 가격초과 옵션 제거 사용 여부 "state_key": "remove_overprice", "dependents": { # ON/OFF별 enable 위젯 리스트 "on": [], "off": [] }, "visible": { # ON/OFF별 visible 위젯 리스트 "on": [], "off": [], } }, "cmb_button": { "key": "price/cmb_button", # 가격설정 다이알로그 버튼 "dependents": { # ON/OFF별 enable 위젯 리스트 "on": [], "off": [] }, "visible": { # ON/OFF별 visible 위젯 리스트 "on": [], "off": [], } }, # 썸네일 "thumb_toggle": { "key": "thumb/thumb_toggle_enabled", # 썸네일 번역 사용 여부 "state_key": "thumb", "dependents": { # ON/OFF별 enable 위젯 리스트 "on": [], "off": [] }, "visible": { # ON/OFF별 visible 위젯 리스트 "on": [], "off": [], } }, "thumb_trans_type_label": { "key": "thumb/thumb_trans_type_label", # 썸네일 번역 타입 라벨 "dependents": { # ON/OFF별 enable 위젯 리스트 "on": [], "off": [] }, "visible": { # ON/OFF별 visible 위젯 리스트 "on": [], "off": [], } }, "thumb_nukki_toggle": { "key": "thumb/thumb_nukki_enabled", # 썸네일 배경제거 사용 여부 "state_key": "thumb_nukki", "dependents": { # ON/OFF별 enable 위젯 리스트 "on": ["thumb_rmb_count_input"], "off": [] }, "visible": { # ON/OFF별 visible 위젯 리스트 "on": [], "off": [], } }, "thumb_rmb_count_input": { "key": "thumb/thumb_rmb_count_input_enabled", # 썸네일 배경제거 사용 여부 "state_key": "thumb_rmb_count", "dependents": { # ON/OFF별 enable 위젯 리스트 "on": [], "off": [] }, "visible": { # ON/OFF별 visible 위젯 리스트 "on": [], "off": [], } }, "nukki_server_toggle": { "key": "thumb/nukki_server_toggle", # 누끼 서버 사용 여부 "state_key": "use_local_rembg", "dependents": { # ON/OFF별 enable 위젯 리스트 "on": [], "off": [] }, "visible": { # ON/OFF별 visible 위젯 리스트 "on": [], "off": [], } }, # 상세페이지 "detail_Option_toggle": { "key": "detail/detail_option_toggle", # 상세페이지 옵션 사용 여부 "state_key": "detail_Option", "dependents": { # ON/OFF별 enable 위젯 리스트 "on": [], "off": [] }, "visible": { # ON/OFF별 visible 위젯 리스트 "on": ["detail_text_button"], "off": [], } }, "detail_text_button": { "key": "detail/detail_text_button", # 상세페이지 텍스트 수정 버튼 "dependents": { # ON/OFF별 enable 위젯 리스트 "on": [], "off": [] }, "visible": { # ON/OFF별 visible 위젯 리스트 "on": [], "off": [], } }, "detail_IMGTrans_toggle": { "key": "detail/detail_img_trans_enabled", # 상세페이지 이미지 번역 사용 여부 "state_key": "detail_IMGTrans", "dependents": { # ON/OFF별 enable 위젯 리스트 "on": [], "off": [] }, "visible": { # ON/OFF별 visible 위젯 리스트 "on": [ "watermark_toggle_label", "watermark_toggle", "watermark_text_input_label", "watermark_text_input", "opacity_percent_input_label", "opacity_percent_input", "detail_concurrency_widget" ], "off": [], } }, "detail_IMGTrans_type_label": { "key": "detail/detail_IMGTrans_type_label", # 상세페이지 이미지 번역 타입 라벨 "dependents": { # ON/OFF별 enable 위젯 리스트 "on": [], "off": [] }, "visible": { # ON/OFF별 visible 위젯 리스트 "on": [], "off": [], } }, "watermark_toggle_label": { "key": "detail/watermark_toggle_label", # 상세페이지 이미지 워터마크 사용 여부 "dependents": { # ON/OFF별 enable 위젯 리스트 "on": [], "off": [], }, "visible": { # ON/OFF별 visible 위젯 리스트 "on": [], "off": [], } }, "watermark_toggle": { "key": "detail/watermark", # 상세페이지 이미지 워터마크 사용 여부 "state_key": "watermark", "dependents": { # ON/OFF별 enable 위젯 리스트 "on": ["watermark_text_input", "opacity_percent_input"], "off": [], }, "visible": { # ON/OFF별 visible 위젯 리스트 "on": [], "off": [], } }, "vip_detail_edit_toggle": { "key": "detail/vip_detail_edit_enabled", # VIP 등록모드 상세페이지 수정 사용 여부 "state_key": "vip_detail_edit", "dependents": { # ON/OFF별 enable 위젯 리스트 "on": [], "off": [] }, "visible": { # ON/OFF별 visible 위젯 리스트 "on": [], "off": [], } }, "watermark_text_input_label": { "key": "detail/watermark_text_input_label", # 상세페이지 이미지 워터마크 텍스트 "dependents": { # ON/OFF별 enable 위젯 리스트 "on": [], "off": [] }, "visible": { # ON/OFF별 visible 위젯 리스트 "on": [], "off": [], } }, "watermark_text_input": { "key": "detail/watermark_text_input", # 상세페이지 이미지 워터마크 텍스트 "state_key": "watermark_text", "dependents": { # ON/OFF별 enable 위젯 리스트 "on": [], "off": [] }, "visible": { # ON/OFF별 visible 위젯 리스트 "on": [], "off": [], } }, "opacity_percent_input_label": { "key": "detail/opacity_percent_input_label", # 상세페이지 이미지 워터마크 투명도 설정 "dependents": { # ON/OFF별 enable 위젯 리스트 "on": [], "off": [] }, "visible": { # ON/OFF별 visible 위젯 리스트 "on": [], "off": [], } }, "opacity_percent_input": { "key": "detail/opacity_percent_input", # 상세페이지 이미지 워터마크 투명도 설정 "state_key": "opacity_percent", "dependents": { # ON/OFF별 enable 위젯 리스트 "on": [], "off": [] }, "visible": { # ON/OFF별 visible 위젯 리스트 "on": [], "off": [], } }, "detail_concurrency_widget": { "key": "detail/detail_concurrency_widget", "dependents": { "on": [], "off": [] }, "visible": { "on": ["detail_concurrency_label", "detail_concurrency_spinbox"], "off": [] } }, "detail_concurrency_label": { "key": "detail/detail_concurrency_label", "dependents": { "on": [], "off": [] }, "visible": { "on": [], "off": [] } }, "detail_concurrency_spinbox": { "key": "detail/detail_concurrency_limit", "state_key": "detail_concurrency_limit", "dependents": { "on": [], "off": [] }, "visible": { "on": [], "off": [] } }, "detail_promo_enabled_toggle": { "key": "detail/detail_promo_enabled", "state_key": "detail_promo_enabled", "dependents": { "on": ["detail_promo_position_widget"], "off": [] }, "visible": { "on": ["detail_promo_position_widget"], "off": [] } }, "detail_promo_position_widget": { "key": "detail/detail_promo_position_widget", "dependents": { "on": [], "off": [] }, "visible": { "on": ["detail_promo_position_label", "detail_promo_position_combo"], "off": [] } }, "detail_promo_position_label": { "key": "detail/detail_promo_position_label", "dependents": { "on": [], "off": [] }, "visible": { "on": [], "off": [] } }, "detail_promo_position_combo": { "key": "detail/detail_promo_position", "state_key": "detail_promo_position", "type": "text", "dependents": { "on": [], "off": [] }, "visible": { "on": [], "off": [] } }, # 기타 "discord_notify_toggle": { "key": "etc/discord_notify_toggle", # 디스코드 알림 사용 여부 "state_key": "discord", "dependents": { # ON/OFF별 enable 위젯 리스트 "on": [], "off": [] }, "visible": { # ON/OFF별 visible 위젯 리스트 "on": ["webhook_input_label", "webhook_input"], "off": [], } }, "webhook_input_label": { "key": "etc/webhook_input_label", # 디스코드 웹훅 입력 라벨 "dependents": { # ON/OFF별 enable 위젯 리스트 "on": [], "off": [] }, "visible": { # ON/OFF별 visible 위젯 리스트 "on": [], "off": [], } }, "webhook_input": { "key": "etc/webhook_input", # 디스코드 웹훅 입력 "state_key": "discord_webhook", "dependents": { # ON/OFF별 enable 위젯 리스트 "on": [], "off": [] }, "visible": { # ON/OFF별 visible 위젯 리스트 "on": [], "off": [], } }, "gpt_model_label": { "key": "etc/gpt_model_label", # GPT 모델 선택 "dependents": { # ON/OFF별 enable 위젯 리스트 "on": [], "off": [] }, "visible": { # ON/OFF별 visible 위젯 리스트 "on": [], "off": [], } }, "gpt_model": { "key": "etc/gpt_model_combo", # GPT 모델 선택 "state_key": "gpt_model", "type": "data", "dependents": { # ON/OFF별 enable 위젯 리스트 "on": [], "off": [] }, "visible": { # ON/OFF별 visible 위젯 리스트 "on": [], "off": [], } }, "requests_server_type": { "key": "etc/requests_server_type_combo", # 자체서버 종류 선택 "state_key": "requests_server_type", "type": "data", "dependents": { # ON/OFF별 enable 위젯 리스트 "on": [], "off": [] }, "visible": { # ON/OFF별 visible 위젯 리스트 "on": [], "off": [], } }, } def init_settings(self): self.login_infos={ 'admin_id' : None, 'admin_pw' : None, 'user_id' : None, 'user_pw' : None, 'is_admin' : False, } # 토글 상태 초기화 self.toggle_states = { 'force_cpu_ocr': True, 'title': False, 'title_shuffle': False, 'title_trans_type': False, 'del_warning_word': False, 'collect_method_combo': "api", 'ocr': False, 'unwanted_words': False, 'interval': 3.0, 'watingTime': 20, 'memo': False, 'memo_toggle_exposer': False, 'memo_toggle_order' : False, 'optionTrnas': False, 'optionTrnas_method': True, 'optionNumbering_method_type': "alphabetic_upper", 'optionIMGTrans': False, 'optionIMGTrans_type': None, 'optionAutoSelect': False, 'optionName_max_length': 22, 'option_numbering_shuffle': False, 'first_option_img_to_thumb': False, 'price': False, 'price_range_percent': 1, 'tag': False, 'tag_method': 'product_name', # 'product_name', 'ai', 'keyword' 'tag_ai': False, 'tag_by_product_name': True, 'tag_lens': False, 'delete_all_tags': False, 'thumb': False, 'thumb_represent': False, 'thumb_trans_type': None, 'thumb_nukki': False, 'remove_background_white': True, 'detail_Option': False, 'detail_IMGTrans': False, 'detail_IMGTrans_type': None, 'detail_concurrency_limit': 2, "detail_promo_enabled" : True, "detail_promo_position" : "top", 'debug_mode': False, 'ed_mode': False, 'discord': False, 'is_localServer': False, 'watermark_toggle': False, 'vip_detail_edit': False, 'clientID': "", 'clientSecret': "", 'gpt_model': "gpt-5-nano", 'requests_server_type': "main", # 자체서버 종류 (main: 메인서버, test: 테스트서버) 'discord_webhook': "", 'watermark_text': "", 'thumb_rmb_count': 1, 'max_option_count': 20, 'opacity_percent': 20, 'group_index': 1, 'remove_overprice': False, # 가격초과제외 기능 'cat_rec': False, # 카테 추천 기능 'fixed_keywords': False, # 키고정 기능 'fixed_keywords_count': 3, # 키고정 개수 기본값 'sub_word_remove': False, # 포함 단어 삭제 기능 (예: '식탁의자' 있으면 '의자' 삭제) 'del_warning_word': False, # 경고 단어 삭제 버튼 클릭 여부 'title_length_limit': 35, # 상품명 최대 길이 'forbidden_partial_title': False, # 상품명 금지어 포함 매칭 여부 'forbidden_partial_option': False, # 옵션 금지어 포함 매칭 여부 'forbidden_partial_tag': False, # 태그 금지어 포함 매칭 여부 'base_dir': "", 'TEMP_IMAGE_DIR': "", 'ERROR_SCREENSHOT_DIR': "", 'font_type': "폰트1", 'image_font_path': "", 'watermark_font_path': "", 'request_inpainting_server_url': "", 'request_inpainting_server_url_local': "", 'request_rembg_server_url': "", 'request_rembg_server_url_local': "", 'local_rembg_model_path': "", 'gemma_api_base_url': "", 'gemma_api_base_url_local': "", 'gemma_api_timeout': 120, 'gemma_api_timeout_local': 120, 'membership_level': "basic", 'image_worker_restart_every': 5, # 5개 이미지마다 재시작 (덜 자주) 'image_worker_restart_count': 0, 'products_per_context_restart': 20, # 메모리 관리 강화 설정 'image_worker_mem_restart_threshold': 85, # 85% 메모리 사용 시 재시작 (여유롭게 조정) 'image_worker_mem_error_escalate_after': 2, # 2회 메모리 오류 시 재시작 (덜 공격적으로) 'enable_aggressive_memory_cleanup': True, # 강력한 메모리 정리 활성화 'force_cuda_cache_clear': True, # CUDA 캐시 강제 정리 'use_local_rembg': False, 'local_model_name': "birefnet-general-lite", 'output_image_format': "webp", "inpaint_method": "external_request", "min_masks_for_lama": 2, "use_roi_optimized_mask": True, # True: 새 방식, False: 기존 방식 "enable_mask_refinement": False, # ROI 마스크 정제 비활성화 "context_expansion_ratio": 0.4, # 최소 확장 "blend_mode": "simple", # 단순 블렌딩 "performance_mode": True, # 빠른 경로 사용 "max_image_size": 1280, # 더 작은 크기 제한 "roi_area_high": 0.0, # 기본값: 0.60 → 0.0으로 변경 # 풀프레임 인페인팅 강제 "local_inpaint_method": "migan", "migan_onnx_path": "", "migan_use_cuda": True, # MIGAN CUDA 사용 (use_cuda=True일때 자동 활성화) "migan_intra_threads": 0, "migan_inter_threads": 0, "migan_use_tensorrt": True, "migan_trt_fp16_enable": True, "migan_max_image_size": 2048, # GPU/CUDA 전역 설정 "use_cuda": True, # 전체 CUDA 사용 여부 (RTX 3050 8GB 환경에서 권장) } def on_font_type_combo_changed(self, selected_option): """폰트 타입 드롭박스 변경 핸들러""" validated_option = selected_option.strip() # self.settings_manager.save_value("font_type", validated_option) self.universal_input_handler(self.font_type_combo, validated_option) # self.show_font_preview(validated_option) def on_tag_method_changed(self): """태그 생성 방식 드롭박스 변경 핸들러""" selected_data = self.tag_method_combo.currentData() # toggle_states 업데이트 self.toggle_states['tag_method'] = selected_data # 선택된 방식에 따라 관련 토글 상태 업데이트 if selected_data == "product_name": # 상품명기반생성 self.toggle_states['tag_by_product_name'] = True self.toggle_states['tag_ai'] = False self.toggle_states['tag_lens'] = False # tag는 그대로 유지 (메인 토글) elif selected_data == "ai": # AI태그생성 self.toggle_states['tag_by_product_name'] = False self.toggle_states['tag_ai'] = True self.toggle_states['tag_lens'] = False elif selected_data == "keyword": # 기존태그방식 self.toggle_states['tag_by_product_name'] = False self.toggle_states['tag_ai'] = False self.toggle_states['tag_lens'] = False elif selected_data == "lens": # 렌즈기반태그 (VIP 전용) self.toggle_states['tag_by_product_name'] = False self.toggle_states['tag_ai'] = False self.toggle_states['tag_lens'] = True # 설정 저장 self.settings_manager.save_value("tag/tag_method_combo", selected_data) self.universal_input_handler(self.tag_method_combo, selected_data) # ------------------------------------------------------------------ # 폰트 미리보기 헬퍼 메서드 # ------------------------------------------------------------------ def load_font_data(self): """폰트 정보 JSON 파일을 로드합니다.""" json_path = os.path.join(self.base_dir, "modules", "fonts", "fonts.json") if not os.path.exists(json_path): if hasattr(self, 'logger'): self.logger.log(f"[load_font_data] 폰트 설정 파일이 없습니다: {json_path}", level=logging.WARNING) return [] try: with open(json_path, 'r', encoding='utf-8') as f: data = json.load(f) return data except Exception as e: if hasattr(self, 'logger'): self.logger.log(f"[load_font_data] 폰트 설정 로드 중 오류: {e}", level=logging.ERROR) return [] def open_font_gallery(self): """폰트 갤러리 다이얼로그를 엽니다.""" font_data = self.load_font_data() if not font_data: QMessageBox.warning(self, "알림", "불러올 폰트 정보가 없습니다.") return # 현재 설정된 폰트값 가져오기 current_font_id = self.settings_manager.get_value("font_type") if not current_font_id: current_font_id = "폰트1" dlg = FontSelectionDialog(font_data, self.base_dir, current_font_id=current_font_id, parent=self) if dlg.exec_() == QDialog.Accepted: selected_id = dlg.selected_font_id if selected_id: # 설정 저장 self.settings_manager.save_value("font_type", selected_id) # 폰트 이름 찾기 font_name = selected_id for f in font_data: if f['id'] == selected_id: font_name = f['name'] break # 버튼 텍스트 업데이트: "폰트1:폰트이름 [폰트선택]" btn_text = f"{selected_id}:{font_name} [폰트선택]" self.font_select_btn.setText(btn_text) # toggle_states 업데이트 (다른 모듈 연동용) state_key = "font_type" self.toggle_states[state_key] = selected_id self.logger.log(f"[open_font_gallery] 폰트 변경됨: {selected_id}", level=logging.INFO) # def validate_and_adjust_option_old(self, combo_box, selected_option, option_type): # """선택된 옵션이 사용자 등급에 맞는지 확인하고 조정합니다.""" # current_tier = self.user_membership_level # allowed_options = self.get_allowed_options_for_tier(current_tier, option_type) # if selected_option not in allowed_options: # best_option = self.get_best_option_for_tier(current_tier, option_type) # # 등급별 메시지 표시 # tier_names = {'basic': 'Basic', 'premium': 'Premium', 'vip': 'VIP'} # current_tier_name = tier_names.get(current_tier, 'Basic') # QMessageBox.information( # self, # "등급 제한", # f"{current_tier_name} 등급에서는 '{selected_option}' 옵션을 사용할 수 없습니다.\n" # f"사용 가능한 옵션: {', '.join(allowed_options)}\n" # f"'{best_option}' 옵션으로 자동 설정됩니다." # ) # try: # idx = combo_box.findData(best_option) # if idx >= 0: # combo_box.setCurrentIndex(idx) # else: # combo_box.setCurrentText(best_option) # 혹시 매핑 누락 시 최후 수단 # except Exception as e: # self.logger.log(f"옵션 설정 오류: {e}", level=logging.ERROR) # combo_box.setCurrentText(best_option) # 혹시 매핑 누락 시 최후 수단 # return best_option # return selected_option def validate_and_adjust_option(self, combo_box, selected_option, option_type): """ 선택된 옵션이 사용자 등급에 맞는지 확인하고 조정합니다. - 콤보박스가 label/userData(코드) 구조이든, 텍스트만 있든 모두 지원 - 비교/정책 검증은 '코드' 기준, 메시지는 '라벨' 기준 - gpt_model, translation(CPU/GPU/자체서버) 공통 처리 """ # 1) 콤보 내용을 한 번만 훑어서 code↔label 매핑을 만든다. # userData가 없으면 label 자체를 code로 간주. code_to_label = {} label_to_code = {} for i in range(combo_box.count()): label = combo_box.itemText(i) code = combo_box.itemData(i) if code is None: code = label code_to_label[code] = label label_to_code[label] = code # 2) 선택값을 '코드'로 통일 # - selected_option이 코드일 수도, 라벨일 수도 있음 idx = combo_box.findData(selected_option) if idx >= 0: selected_code = combo_box.itemData(idx) or combo_box.itemText(idx) else: idx = combo_box.findText(selected_option) if idx >= 0: selected_code = combo_box.itemData(idx) or combo_box.itemText(idx) else: selected_code = selected_option # 콤보에 없으면 입력 그대로 코드로 취급 # 3) 허용 옵션 (코드 리스트) 조회 current_tier = getattr(self, "user_membership_level", "basic") allowed_codes = self.get_allowed_options_for_tier(current_tier, option_type) # 3-1) 빈 값(미설정) 처리: 경고 없이 기본값으로 자동 설정 if not str(selected_code or "").strip(): best_code = self.get_best_option_for_tier(current_tier, option_type) set_idx = combo_box.findData(best_code) if set_idx >= 0: combo_box.setCurrentIndex(set_idx) else: best_label = code_to_label.get(best_code, best_code) combo_box.setCurrentText(best_label) if hasattr(self, "logger"): self.logger.debug( f"[validate_and_adjust_option] 빈값 감지 tier={current_tier}, type={option_type}, 자동='{best_code}'" ) return best_code # 4) 소문자 비교로 허용 여부 판단 sel_norm = (selected_code or "").strip().lower() allowed_norm = {(c or "").strip().lower() for c in allowed_codes} if sel_norm not in allowed_norm: # 4-1) 최고 옵션 코드 선택 best_code = self.get_best_option_for_tier(current_tier, option_type) # 4-2) 메시지는 라벨로 친절하게 표기 tier_names = {'basic': 'Basic', 'premium': 'Premium', 'vip': 'VIP'} tier_name = tier_names.get((current_tier or "").lower(), 'Basic') allowed_labels = [code_to_label.get(c, c) for c in allowed_codes] best_label = code_to_label.get(best_code, best_code) selected_label_for_msg = code_to_label.get(selected_code, selected_option) if not str(selected_label_for_msg or "").strip(): selected_label_for_msg = "미설정" QMessageBox.information( self, "등급 제한", f"{tier_name} 등급에서는 '{selected_label_for_msg}' 옵션을 사용할 수 없습니다.\n" f"사용 가능한 옵션: {', '.join(allowed_labels)}\n" f"'{best_label}' 옵션으로 자동 설정됩니다." ) # 4-3) 콤보를 '코드'로 선택(라벨/코드 분리든 아니든 작동) set_idx = combo_box.findData(best_code) if set_idx >= 0: combo_box.setCurrentIndex(set_idx) else: combo_box.setCurrentText(best_label) if hasattr(self, "logger"): self.logger.debug( f"[validate_and_adjust_option] tier={current_tier}, type={option_type}, 선택='{selected_code}' → 자동='{best_code}'" ) return best_code # 5) 허용된 경우: 콤보 선택 동기화(코드 기준으로 우선) ok_idx = combo_box.findData(selected_code) if ok_idx >= 0: combo_box.setCurrentIndex(ok_idx) else: combo_box.setCurrentText(code_to_label.get(selected_code, selected_option)) return selected_code def get_allowed_options_for_tier(self, tier, option_type): """등급별로 허용되는 옵션들을 반환합니다. (옵션타입별 기본값 적용, 대소문자 안전)""" # 원본 테이블은 그대로 유지 tier_options = { 'basic': { 'translation': ['CPU'], 'gpt_model': ['gpt-5-nano'] }, 'premium': { 'translation': self.get_translation_engine_options(), 'gpt_model': ['gpt-5-nano', 'gpt-4o-mini'] }, 'vip': { 'translation': self.get_translation_engine_options(), # VIP는 GPT 모델 + Grok 모델 모두 사용 가능 'gpt_model': ['gpt-5-nano', 'gpt-4o-mini', 'gpt-4.1-nano', 'grok-4-1-fast-reasoning'] } } # ✅ 옵션 타입별 기본값을 분리(이전의 ['CPU'] 고정 문제 해결) defaults = { 'translation': ['CPU'], 'gpt_model': ['gpt-5-nano'] } t = (tier or "").strip().lower() ot = (option_type or "").strip().lower() # 키도 소문자 기준이므로, 안전하게 조회 options_by_tier = tier_options.get(t, {}) return options_by_tier.get(ot, defaults.get(ot, [])) def get_best_option_for_tier(self, tier, option_type): """등급별로 가장 좋은 옵션을 반환합니다.""" allowed_options = self.get_allowed_options_for_tier(tier, option_type) if option_type == "gpt_model": # Grok4 > gpt-4.1-nano > gpt-4o-mini > gpt-5-nano 순서로 최고 모델 선택 # VIP는 Grok 모델이 최고 옵션 for option in ['grok-4-1-fast-reasoning', 'gpt-4.1-nano', 'gpt-4o-mini', 'gpt-5-nano']: if option in allowed_options: return option return 'gpt-5-nano' else: # translation (번역방식) # 자체서버 > CPU 순서로 최고방식 선택 for option in ['자체서버', 'GPU', 'CPU']: if option in allowed_options: return option return 'CPU' def on_onnx_model_type_combo_changed(self, selected_option): """ONNX 모델 타입 콤보박스 변경 핸들러""" self.logger.log(f"ONNX 모델 타입 변경: {selected_option}", level=logging.DEBUG) # 자동 선택이면 현재 GPU 분석하여 추천 모델 표시 if selected_option == "자동 선택": gpu_info = self.get_gpu_info() recommended = gpu_info['recommended_model'].upper() # 추천 모델을 로그에 표시 model_names = {'fp16': 'FP16', 'opt': 'OPT', 'simp': 'SIMP'} recommended_name = model_names.get(gpu_info['recommended_model'], 'OPT') self.logger.log(f"자동 선택 - GPU: {gpu_info['model_name']}, 추천 모델: {recommended_name}", level=logging.INFO) # 선택사항에 따라 툴팁 업데이트 tooltip_text = f"현재 GPU ({gpu_info['vendor'].upper()} {gpu_info['gpu_type']})에 최적화된 {recommended_name} 모델이 자동 선택됩니다." self.onnx_model_type_combo.setToolTip(tooltip_text) # toggle_states 업데이트 self.toggle_states['onnx_model_type'] = selected_option self.universal_input_handler(self.onnx_model_type_combo, selected_option) def on_optionNumbering_method_combo_changed(self, index): """옵션 넘버링 방식 콤보박스 변경 핸들러""" if index < 0: return # itemData로 내부 값 가져오기 selected_value = self.optionNumbering_method_combo.itemData(index) if selected_value: self.logger.log(f"옵션 넘버링 방식 변경: {selected_value}", level=logging.DEBUG) self.universal_input_handler(self.optionNumbering_method_combo, selected_value) def get_selected_onnx_model_type(self): """현재 선택된 ONNX 모델 타입을 반환 (자동 선택일 경우 추천 모델 반환)""" selected = self.onnx_model_type_combo.currentText() if selected == "자동 선택": gpu_info = self.get_gpu_info() return gpu_info['recommended_model'] # 'fp16', 'opt', 'simp' else: # UI 텍스트를 내부 코드로 변환 model_map = { "FP16 모델": "fp16", "OPT 모델": "opt", "SIMP 모델": "simp" } return model_map.get(selected, "opt") # 기본값 opt def get_onnx_model_filename(self, base_name, model_type=None): """ ONNX 모델 파일명을 생성합니다. Args: base_name (str): 기본 모델명 (예: "translator") model_type (str): 모델 타입 ('fp16', 'opt', 'simp'), None이면 설정에서 자동 선택 Returns: str: 완전한 모델 파일명 (예: "translator.fp16.onnx") """ if model_type is None: model_type = self.get_selected_onnx_model_type() return f"{base_name}.{model_type}.onnx" def get_onnx_model_fallback_list(self, base_name, preferred_type=None): """ ONNX 모델 폴백 순서 리스트를 반환합니다. Args: base_name (str): 기본 모델명 preferred_type (str): 선호 모델 타입, None이면 설정에서 자동 선택 Returns: list: 폴백 순서대로 정렬된 모델 파일명 리스트 """ if preferred_type is None: preferred_type = self.get_selected_onnx_model_type() # GPU 정보 기반 폴백 순서 결정 gpu_info = self.get_gpu_info() # 기본 폴백 순서: 선호 모델 → 추천 모델 → 안정성 순 fallback_order = [] # 1순위: 사용자가 선택한 모델 fallback_order.append(f"{base_name}.{preferred_type}.onnx") # 2순위: GPU 분석 기반 추천 모델 (선호 모델과 다른 경우) if gpu_info['recommended_model'] != preferred_type: fallback_order.append(f"{base_name}.{gpu_info['recommended_model']}.onnx") # 3순위: 안정성 기반 폴백 stability_order = ['opt', 'simp', 'fp16'] # 안정성 순서 for model_type in stability_order: filename = f"{base_name}.{model_type}.onnx" if filename not in fallback_order: fallback_order.append(filename) self.logger.log(f"ONNX 모델 폴백 순서: {fallback_order}", level=logging.DEBUG) return fallback_order def get_model_loading_strategy(self): """ 현재 환경에 맞는 모델 로딩 전략을 반환합니다. Returns: dict: 로딩 전략 정보 """ gpu_info = self.get_gpu_info() selected_type = self.get_selected_onnx_model_type() strategy = { 'gpu_info': gpu_info, 'selected_model': selected_type, 'use_gpu': gpu_info['has_directx12'], 'fallback_to_cpu': True, 'model_priority': self.get_onnx_model_fallback_list("model", selected_type), 'timeout_seconds': 30, 'memory_limit_mb': 4096 if gpu_info['gpu_type'] == 'integrated' else 8192 } self.logger.log(f"모델 로딩 전략: {strategy}", level=logging.DEBUG) return strategy def on_gpt_model_combo_changed(self, selected_option): """GPT/Grok 모델 드롭박스 변경 핸들러""" if self.authenticated_by_admin: validated_option = selected_option else: validated_option = self.validate_and_adjust_option( self.gpt_model_combo, selected_option, "gpt_model" ) # Grok 모델 선택 시 로그 출력 if validated_option and validated_option.startswith("grok-"): self.logger.log(f"🚀 Grok 모델 선택됨: {validated_option} (VIP 전용)", level=logging.INFO) else: self.logger.log(f"GPT 모델 선택됨: {validated_option}", level=logging.DEBUG) self.universal_input_handler(self.gpt_model_combo, validated_option) def on_requests_server_type_combo_changed(self, selected_code): """자체서버 종류 선택이 변경될 때 호출되는 함수""" self.logger.log(f"자체서버 종류 변경: {selected_code}", level=logging.DEBUG) self.toggle_states['requests_server_type'] = selected_code self.universal_input_handler(self.requests_server_type_combo, selected_code) def universal_input_handler(self, widget, *args): """ 어떤 위젯에서든 호출되는 범용 입력 핸들러. 위젯 타입에 따라 알맞은 개별 핸들러로 분기. """ # 토글/체크박스/콤보박스 (on/off 또는 선택값) if isinstance(widget, QComboBox): # 콤보 항목이 없는 상태에서의 호출은 무시 (재구성 중 보호) if widget.count() == 0: return self.handle_combobox_input(widget) elif 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_combobox_input(self, widget): """QComboBox 위젯의 값 변경을 처리합니다.""" widget_name = widget.objectName() selected_text = widget.currentText() config = self.widget_map.get(widget_name) if not config: self.logger.log(f"[handle_combobox_input] '{widget_name}' 위젯맵에 없음.", level=logging.WARNING) return # 설정 키와 상태 키 가져오기 settings_key = config.get("key", widget_name) state_key = self.get_state_key(widget_name) # GPT 모델 콤보박스일 경우: 표시값 → 내부값으로 변환 if widget_name == "gpt_model": current_index = widget.currentIndex() selected_value = None # currentData() 먼저 시도 if current_index >= 0: selected_value = widget.currentData() self.logger.log(f"[GPT Model Debug] currentIndex={current_index}, currentData={selected_value}, currentText={selected_text}", level=logging.DEBUG) # currentData()가 None이거나 비어있으면 fallback if not selected_value: selected_value = selected_text self.logger.log(f"[GPT Model Debug] currentData가 비어있음, fallback to text: {selected_value}", level=logging.WARNING) elif widget_name == "optionNumbering_method_combo": # 옵션 넘버링 방식 콤보박스: 내부값(currentData) 사용 current_index = widget.currentIndex() selected_value = None if current_index >= 0: selected_value = widget.currentData() self.logger.log(f"[옵션 넘버링 Debug] currentIndex={current_index}, currentData={selected_value}, currentText={selected_text}", level=logging.DEBUG) # currentData()가 None이거나 비어있으면 fallback if not selected_value: selected_value = "alphabetic_upper" # 기본값 self.logger.log(f"[옵션 넘버링 Debug] currentData가 비어있음, 기본값 사용: {selected_value}", level=logging.WARNING) else: # type: data 인 경우 currentData() 사용 config_type = config.get("type", "text") if config_type == "data": selected_value = widget.currentData() if selected_value is None: selected_value = selected_text else: selected_value = selected_text # 설정 및 상태 저장 self.settings_manager.save_value(settings_key, selected_value) self.toggle_states[state_key] = selected_value self.logger.log( f"[ComboBox Handler] '{widget_name}' 값 변경: 표시값='{selected_text}', 저장값='{selected_value}', State Key='{state_key}'", level=logging.DEBUG ) # 의존성 있는 위젯 활성화/비활성화 처리 & 중복 저장(handle_toggle_state 내부) # 모든 위젯에 대해 올바른 selected_value(데이터값) 전달 self.handle_toggle_state(widget, selected_value) 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 if value is None: config_type = config.get("type", "text") if hasattr(widget, "isChecked"): value = widget.isChecked() elif config_type == "data" and hasattr(widget, "currentData"): value = widget.currentData() elif config_type != "data" and 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 # 폰트 선택 버튼 및 라벨 초기화 (settings_manager 값 반영) if hasattr(self, "font_select_btn"): # "font_type_combo" 위젯이 사라졌지만 설정값은 기존 키를 쓰거나 새로운 키를 쓸 수 있음. # 여기서는 "font_type"을 사용 (open_font_gallery에서 저장한 키) font_val = self.settings_manager.get_value("font_type") if not font_val: font_val = "폰트1" self.toggle_states["font_type"] = font_val # 버튼 텍스트 초기화 font_data = self.load_font_data() font_name = font_val if font_data: for f in font_data: if f['id'] == font_val: font_name = f['name'] break self.font_select_btn.setText(f"{font_val}:{font_name} [폰트선택]") 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 update_toggle_ui(self): for widget_name, config in self.widget_map.items(): widget = getattr(self, widget_name, None) if not widget: self.logger.log(f"[update_toggle_ui] '{widget_name}' 위젯 없음.", level=logging.DEBUG) continue set_key = config.get("key", widget_name) val = self.settings_manager.get_value(set_key) self.logger.log(f"[update_toggle_ui] set_key: {set_key}, widget_name: {widget_name}, val: {val}", level=logging.DEBUG) # 위젯 값 반영 if hasattr(widget, "setChecked"): if val is not None: widget.setChecked(val in [True, 'true', '1', 1]) elif hasattr(widget, "setValue"): if val is not None: try: widget.setValue(int(val)) except Exception: try: widget.setValue(float(val)) except Exception: pass elif hasattr(widget, "setText"): if val is not None: widget.setText(str(val)) elif hasattr(widget, "setCurrentIndex") and (hasattr(widget, "findData") or hasattr(widget, "findText")): if val is not None: # 콤보박스 복원 로직 combotype = config.get("type", "data") self.logger.log(f"[update_toggle_ui] {widget_name} 콤보박스 타입: {combotype}", level=logging.DEBUG) idx = -1 # 🎯 gpt_model: label/data 어느 쪽이 저장됐든 복원되게 처리 if widget_name == "gpt_model": # 1) 내부값(data)로 먼저 찾기 idx = widget.findData(val) # 2) 못 찾으면 표시값(label)로 찾기 if idx < 0: idx = widget.findText(str(val)) else: # 일반 콤보박스: type 값에 따라 우선순위 결정 if combotype == "data": idx = widget.findData(val) if idx < 0: idx = widget.findText(str(val)) else: idx = widget.findText(str(val)) if idx < 0: idx = widget.findData(val) if idx >= 0: widget.setCurrentIndex(idx) else: self.logger.log( f"[update_toggle_ui] '{widget_name}'에서 저장값 '{val}'로 항목을 찾지 못했습니다.", level=logging.WARNING ) # gpt_model은 내부값(data) 기준으로 토글 계산 필요 if widget_name == "gpt_model": current_val = widget.currentData() # 내부값 self.universal_input_handler(widget, current_val) else: self.universal_input_handler(widget, val) # 종속 상태 반영 self.universal_input_handler(widget, val) # self.logger.log(f"한시적 누끼토글 OFF", level=logging.INFO) # self.thumb_nukki_toggle.setChecked(False) # self.thumb_nukki_toggle.setEnabled(False) # self.toggle_states['thumb_nukki'] = False # self.thumb_rmb_count_input.setValue(0) # self.thumb_rmb_count_input.setEnabled(False) # self.toggle_states['thumb_rmb_count'] = 0 # self.optionTrnas_method_toggle.setEnabled(True) # self.optionTrnas_method_toggle.setChecked(True) # self.toggle_states['optionTrnas_method'] = True def on_title_toggle_for_interlock(self, checked): if checked: # 렌즈가 ON → API는 강제로 OFF self.title_shuffle_toggle.setChecked(False) # else: # # 둘 다 OFF가 되지 않도록 강제 ON # if not self.title_shuffle_toggle.isChecked(): # self.title_shuffle_toggle.setChecked(True) def on_title_shuffle_toggle_for_interlock(self, checked): if checked: # API가 ON → 렌즈는 강제로 OFF self.title_toggle.setChecked(False) # else: # if not self.title_toggle.isChecked(): # self.title_toggle.setChecked(True) def on_watermark_toggle_clicked(self, is_checked): """워터마크 토글 여부에 따라 회사 이름 입력 필드와 확인 버튼을 표시/숨김""" if is_checked: self.watermark_text_label.setVisible(True) self.watermark_text_input.setVisible(True) self.opacity_percent_label.setVisible(True) self.opacity_percent_input.setVisible(True) # 워터마크 텍스트 입력 필드의 내용을 딕셔너리에 저장 self.toggle_states['watermark_text'] = self.watermark_text_input.text() else: self.watermark_text_label.setVisible(False) self.watermark_text_input.setVisible(False) self.opacity_percent_label.setVisible(False) self.opacity_percent_input.setVisible(False) def update_watermark_text(self): """QLineEdit에 입력된 텍스트를 toggle_states['watermark_text']에 저장""" self.toggle_states['watermark_text'] = self.watermark_text_input.text() self.logger.log(f"Updated watermark text: {self.toggle_states['watermark_text']}", level=logging.DEBUG) # # 메시지 박스를 통해 업데이트 알림 (값 포함) # self.show_message( # "워터마크 텍스트 업데이트", # f"워터마크 텍스트가 업데이트되었습니다: {self.toggle_states['watermark_text']}" # ) def update_thumb_rmb_count(self, value): """QSpinBox에 입력된 값을 toggle_states['thumb_rmb_count']에 저장""" self.toggle_states['thumb_rmb_count'] = value # 변경된 정수 값을 바로 저장 self.logger.log(f"썸네일 삭제 버튼 개수 업데이트: {self.toggle_states['thumb_rmb_count']}", level=logging.DEBUG) def update_max_option_count(self, value): """QSpinBox에 입력된 값을 toggle_states['max_option_count']에 저장""" self.toggle_states['max_option_count'] = value # 변경된 정수 값을 바로 저장 self.logger.log(f"최대 선택 가능 옵션 수 업데이트: {self.toggle_states['max_option_count']}", level=logging.DEBUG) def update_opacity_percent(self, value): """QSpinBox에 입력된 값을 toggle_states['opacity_percent']에 저장""" self.toggle_states['opacity_percent'] = value # 변경된 정수 값을 바로 저장 self.logger.log(f"워터마크 투명도 업데이트: {self.toggle_states['opacity_percent']}", level=logging.DEBUG) def update_keyword_fix_count(self, value): self.toggle_states['fixed_keywords_count'] = value self.logger.log(f"키워드 고정 개수 업데이트: {value}", level=logging.DEBUG) def update_title_length_limit(self, value): self.toggle_states['title_length_limit'] = value self.logger.log(f"상품명 최대 길이 업데이트: {value}", level=logging.DEBUG) def update_discord_settings(self, checked=None): """ 디스코드 알림 설정 업데이트 - 토글 상태에 따라 웹훅 입력 필드 표시/숨김 - 설정값 저장 """ self.logger.log(f"디스코드 알림 설정 업데이트 시작", level=logging.DEBUG) # 토글 상태 확인 (인자가 없을 경우 현재 상태 사용) is_enabled = checked if checked is not None else self.discord_notify_toggle.isChecked() # 웹훅 입력 필드 가시성 설정 self.webhook_input.setVisible(is_enabled) # 설정값 저장 (toggle_states 딕셔너리 사용) self.toggle_states['discord'] = is_enabled self.toggle_states['discord_webhook'] = self.webhook_input.text() self.discord_manager.set_webhook_url(self.webhook_input.text()) # 로그 출력 self.logger.log(f"디스코드 알림 설정 변경: {'활성화' if is_enabled else '비활성화'}", level=logging.DEBUG) # 토글 텍스트 업데이트 self.discord_notify_toggle_label.setText( "디스코드 알림" + (" ON" if is_enabled else " OFF") ) def update_webhook_url(self): """웹훅 URL 변경 시 설정 저장""" url = self.webhook_input.text().strip() self.toggle_states['discord_webhook'] = url self.discord_manager.set_webhook_url(url) self.logger.log(f"디스코드 웹훅 URL 업데이트됨", level=logging.DEBUG) def update_watermark_visibility(self): """이미지 번역 토글 중 하나라도 켜져 있으면 워터마크 토글을 보이게 하고, visible이 되면 상태에 따라 레이아웃도 제어""" if self.toggle_states['optionIMGTrans'] or self.toggle_states['detail_IMGTrans'] or self.toggle_states['thumb']: # 이미지 번역 토글이 하나라도 켜져 있으면 워터마크 토글 보이기 self.toggle_visibility(True, [(self.watermark_toggle, self.watermark_toggle_label)]) # 워터마크 토글이 보이게 될 때 상태 확인 if self.watermark_toggle.isChecked(): # 워터마크 토글이 ON 상태이면 워터마크 레이아웃도 보이게 함 self.toggle_visibility(True, [(self.watermark_text_input, self.watermark_text_label), (self.opacity_percent_input, self.opacity_percent_label)]) self.watermark_text_input.setFocus() self.watermark_text_input.setEnabled(True) self.opacity_percent_input.setEnabled(True) else: # 워터마크 토글이 OFF 상태이면 워터마크 레이아웃 숨김 self.toggle_visibility(False, [(self.watermark_text_input, self.watermark_text_label), (self.opacity_percent_input, self.opacity_percent_label)]) self.watermark_text_input.setFocus() self.watermark_text_input.setEnabled(False) self.opacity_percent_input.setEnabled(False) else: # 모두 꺼져 있으면 워터마크 토글과 레이아웃 숨기기 self.toggle_visibility(False, [(self.watermark_toggle, self.watermark_toggle_label)]) self.toggle_visibility(False, [(self.watermark_text_input, self.watermark_text_label), (self.opacity_percent_input, self.opacity_percent_label)]) def toggle_visibility(self, is_checked, toggle_items): """ 토글 상태에 따라 여러 필드의 visibility를 제어하는 범용 메서드 :param is_checked: 토글 상태 (True/False) :param toggle_items: 토글 필드와 레이블 목록 [(필드, 레이블), ...] """ for item, label in toggle_items: item.setVisible(is_checked) if label: label.setVisible(is_checked) # def update_api_fields_visibility(self, is_checked): # """ # use_API 토글 버튼 상태에 따라 clientID 및 clientSecretKey 필드를 보이거나 숨기는 메서드 # :param is_checked: use_API 토글 상태 (True/False) # """ # self.toggle_visibility(is_checked, [ # (self.client_id_input, self.client_id_label), # (self.client_secret_input, self.client_secret_label) # ]) # def save_settings(self): # """QSettings에 사용자 정보 저장""" # self.logger.log(f"현재 설정을 저장합니다.", level=logging.DEBUG) # self.settings.setValue("admin/id", self.admin_id_input.text()) # self.settings.setValue("admin/pw", self.admin_pw_input.text()) # self.settings.setValue("user/id", self.user_id_input.text()) # self.settings.setValue("user/pw", self.user_pw_input.text()) # self.settings.setValue("admin/toggle", self.admin_toggle.isChecked()) # self.settings.setValue("watermark_text", self.watermark_text_input.text()) # self.settings.setValue("opacity_percent", self.opacity_percent_input.value()) # self.settings.setValue("max_option_count", self.max_option_count_input.value()) # self.settings.setValue("thumb_rmb_count", self.thumb_rmb_count_input.value()) # # 새로 추가된 토글 버튼 상태 저장 # self.settings.setValue("cat_rec", self.cat_rec_toggle.isChecked()) # self.settings.setValue("fixed_keywords", self.keyword_fix_toggle.isChecked()) # self.settings.setValue("fixed_keywords_count", self.keyword_fix_count_input.value()) # self.settings.setValue("remove_overprice", self.remove_overprice_toggle.isChecked()) # self.settings.setValue("discord", self.discord_notify_toggle.isChecked()) # self.settings.setValue("discord_webhook", self.webhook_input.text()) # self.settings.setValue("title_trans_type", self.title_trans_type_toggle.isChecked()) # self.settings.setValue("title_shuffle", self.title_shuffle_toggle.isChecked()) # def load_settings(self): # """QSettings에서 사용자 정보 불러오기""" # self.admin_id_input.setText(self.settings.value("admin/id", "", type=str)) # self.admin_pw_input.setText(self.settings.value("admin/pw", "", type=str)) # self.user_id_input.setText(self.settings.value("user/id", "", type=str)) # self.user_pw_input.setText(self.settings.value("user/pw", "", type=str)) # admin_toggle_state = self.settings.value("admin/toggle", False, type=bool) # self.admin_toggle.setChecked(admin_toggle_state) # self.on_admin_toggle_clicked(admin_toggle_state) # self.watermark_text_input.setText(self.settings.value("watermark_text", "", type=str)) # self.toggle_states['watermark_text'] = self.watermark_text_input.text() # self.opacity_percent_input.setValue(self.settings.value("opacity_percent", 20, type=int)) # self.toggle_states['opacity_percent'] = int(self.opacity_percent_input.text()) # self.max_option_count_input.setValue(self.settings.value("max_option_count", 20, type=int)) # self.toggle_states['max_option_count'] = int(self.max_option_count_input.text()) # self.thumb_rmb_count_input.setValue(self.settings.value("thumb_rmb_count", 0, type=int)) # self.toggle_states['thumb_rmb_count'] = int(self.thumb_rmb_count_input.text()) # # 상품명 번역 타입 설정 # self.title_trans_type_toggle.setChecked(self.settings.value("title_trans_type", False, type=bool)) # self.toggle_states['title_trans_type'] = self.settings.value("title_trans_type", False, type=bool) # # 상품명 셔플 타입 설정 # self.title_shuffle_toggle.setChecked(self.settings.value("title_shuffle", False, type=bool)) # self.toggle_states['title_shuffle'] = self.settings.value("title_shuffle", False, type=bool) # # 새로 추가된 토글 버튼 상태 불러오기 # cat_rec_state = self.settings.value("cat_rec", False, type=bool) # self.cat_rec_toggle.setChecked(cat_rec_state) # self.toggle_states['cat_rec'] = cat_rec_state # fixed_keywords_state = self.settings.value("fixed_keywords", False, type=bool) # self.keyword_fix_toggle.setChecked(fixed_keywords_state) # self.toggle_states['fixed_keywords'] = fixed_keywords_state # self.keyword_fix_count_input.setValue(self.settings.value("fixed_keywords_count", 2, type=int)) # self.toggle_states['fixed_keywords_count'] = int(self.keyword_fix_count_input.text()) # remove_overprice_state = self.settings.value("remove_overprice", False, type=bool) # self.remove_overprice_toggle.setChecked(remove_overprice_state) # self.toggle_states['remove_overprice'] = remove_overprice_state # # 디스코드 설정 로드 # self.toggle_states['discord'] = self.settings.value("discord", False, type=bool) # self.toggle_states['discord_webhook'] = self.settings.value("discord_webhook", "", type=str) # # 상세 텍스트 버튼 활성화 여부 설정 # self.detail_text_button.setEnabled(self.settings.value("detail_text_button", False, type=bool)) # # UI 요소에 로드된 값 적용 # if hasattr(self, 'discord_notify_toggle'): # self.discord_notify_toggle.setChecked(self.toggle_states['discord']) # if hasattr(self, 'webhook_input'): # self.webhook_input.setText(self.toggle_states['discord_webhook']) # self.webhook_input.setVisible(self.toggle_states['discord']) # self.load_toggle_settings() # self.load_unwanted_words() def save_settings(self): """SettingsManager를 통해 모든 설정 저장""" try: # toggle_states의 값을 SettingsManager에 저장 for key, value in self.toggle_states.items(): if isinstance(value, bool): self.settings_manager.save_value(f"toggle/{key}", value) elif isinstance(value, str): self.settings_manager.save_value(f"input/{key}", value) elif isinstance(value, (int, float)): self.settings_manager.save_value(f"spinbox/{key}", value) # GPU 관련 설정들 별도 저장 (widget_map에 정의되지 않은 것들) gpu_settings = [ 'use_cuda', 'migan_use_cuda', 'migan_intra_threads', 'migan_inter_threads', 'migan_use_tensorrt', 'migan_trt_fp16_enable', 'migan_max_image_size', 'force_cuda_cache_clear', 'migan_onnx_path', 'inpaint_method', 'min_masks_for_lama', 'use_roi_optimized_mask', 'enable_mask_refinement', 'context_expansion_ratio', 'blend_mode', 'performance_mode', 'max_image_size', 'roi_area_high', 'local_inpaint_method' ] for gpu_key in gpu_settings: if gpu_key in self.toggle_states: value = self.toggle_states[gpu_key] self.settings_manager.save_value(f"gpu/{gpu_key}", value) self.logger.log(f"[GPU 설정 저장] {gpu_key}: {value}", level=logging.DEBUG) # UI 위젯 상태도 저장 self.settings_manager.save_settings(self) self.logger.log("설정 저장 완료 (GPU 설정 포함)", level=logging.DEBUG) except Exception as e: self.logger.log(f"설정 저장 중 오류: {e}", level=logging.ERROR) def load_settings(self): """SettingsManager를 통해 모든 설정 불러오기""" try: # SettingsManager를 통해 위젯 상태 복원 self.settings_manager.load_settings(self) # toggle_states를 저장된 설정으로 업데이트 self._load_toggle_states_from_settings() # 콤보박스 설정 복원 self._restore_combobox_settings() # GPU 관련 설정들 별도 로드 self._load_gpu_settings_from_settings() # UI 업데이트 self.update_toggle_ui() # 종속 위젯 상태 적용 self.settings_manager.apply_settings_to_ui(self) # 기타 설정들 self.load_unwanted_words() self.logger.log("설정 불러오기 완료 (GPU 설정 포함)", level=logging.DEBUG) except Exception as e: self.logger.log(f"설정 불러오기 중 오류: {e}", level=logging.ERROR) def _load_toggle_states_from_settings(self): """SettingsManager에서 저장된 설정을 toggle_states에 로드합니다.""" try: # 위젯맵을 통해 각 위젯의 저장된 값을 toggle_states에 로드 for widget_name, config in self.widget_map.items(): state_key = config.get("state_key") if not state_key: continue # state_key가 없는 위젯은 처리하지 않음 # state_key가 toggle_states에 없는 경우 스킵 if state_key not in self.toggle_states: continue # 위젯맵의 key를 사용하여 저장된 값 가져오기 settings_key = config.get("key") if not settings_key: continue saved_value = self.settings_manager.get_value(settings_key) if saved_value is None: continue # 저장된 값이 없으면 스킵 # 타입에 맞게 변환하여 toggle_states 업데이트 current_value = self.toggle_states[state_key] if isinstance(current_value, bool): # bool 타입 변환 if isinstance(saved_value, str): self.toggle_states[state_key] = saved_value.lower() in ['true', '1', 'yes'] else: self.toggle_states[state_key] = bool(saved_value) elif isinstance(current_value, str): # 문자열 타입 self.toggle_states[state_key] = str(saved_value) elif isinstance(current_value, (int, float)): # 숫자 타입 if isinstance(current_value, int): self.toggle_states[state_key] = int(saved_value) else: self.toggle_states[state_key] = float(saved_value) self.logger.log( f"[toggle_states 로드] {widget_name} (state_key: {state_key}): {self.toggle_states[state_key]}", level=logging.DEBUG ) self.logger.log("[SettingsManager] toggle_states 업데이트 완료", level=logging.DEBUG) except Exception as e: self.logger.log(f"[SettingsManager] toggle_states 로드 중 오류: {e}", level=logging.ERROR, exc_info=True) def _restore_combobox_settings(self): """콤보박스 위젯들의 저장된 값을 복원합니다.""" try: # 옵션 넘버링 방식 콤보박스 복원 if hasattr(self, 'optionNumbering_method_combo'): saved_value = self.settings_manager.get_value("option/option_numbering_method_type") if saved_value: # 저장된 값(내부값)으로 콤보박스에서 해당 항목 찾기 for i in range(self.optionNumbering_method_combo.count()): if self.optionNumbering_method_combo.itemData(i) == saved_value: self.optionNumbering_method_combo.setCurrentIndex(i) self.logger.log(f"[콤보박스 복원] 옵션 넘버링 방식: {saved_value}", level=logging.DEBUG) break else: # 값을 찾지 못한 경우 기본값으로 설정 for i in range(self.optionNumbering_method_combo.count()): if self.optionNumbering_method_combo.itemData(i) == "alphabetic_upper": self.optionNumbering_method_combo.setCurrentIndex(i) self.logger.log(f"[콤보박스 복원] 옵션 넘버링 방식: 기본값 설정", level=logging.DEBUG) break # 태그 생성 방식 콤보박스 복원 if hasattr(self, 'tag_method_combo'): saved_value = self.settings_manager.get_value("tag/tag_method_combo") if saved_value: # 저장된 값(내부값)으로 콤보박스에서 해당 항목 찾기 for i in range(self.tag_method_combo.count()): if self.tag_method_combo.itemData(i) == saved_value: self.tag_method_combo.setCurrentIndex(i) self.toggle_states['tag_method'] = saved_value # on_tag_method_changed 호출하여 관련 토글 상태 업데이트 self.on_tag_method_changed() self.logger.log(f"[콤보박스 복원] 태그 생성 방식: {saved_value}", level=logging.DEBUG) break else: # 값을 찾지 못한 경우 기본값으로 설정 (상품명기반생성) for i in range(self.tag_method_combo.count()): if self.tag_method_combo.itemData(i) == "product_name": self.tag_method_combo.setCurrentIndex(i) self.toggle_states['tag_method'] = "product_name" self.on_tag_method_changed() self.logger.log(f"[콤보박스 복원] 태그 생성 방식: 기본값 설정 (product_name)", level=logging.DEBUG) break # 홍보문구 위치 콤보박스 복원 if hasattr(self, 'detail_promo_position_combo'): saved_value = self.settings_manager.get_value("detail/detail_promo_position", "top") if saved_value: # 저장된 값(텍스트)으로 콤보박스에서 해당 항목 찾기 index = self.detail_promo_position_combo.findText(saved_value) if index >= 0: self.detail_promo_position_combo.setCurrentIndex(index) self.toggle_states['detail_promo_position'] = saved_value self.logger.log(f"[콤보박스 복원] 홍보문구 위치: {saved_value}", level=logging.DEBUG) else: # 값을 찾지 못한 경우 기본값으로 설정 self.detail_promo_position_combo.setCurrentIndex(0) # "top"이 첫 번째 항목 self.toggle_states['detail_promo_position'] = "top" self.logger.log(f"[콤보박스 복원] 홍보문구 위치: 기본값 설정 (top)", level=logging.DEBUG) self.logger.log("[콤보박스 복원] 완료", level=logging.DEBUG) except Exception as e: self.logger.log(f"[콤보박스 복원] 오류: {e}", level=logging.ERROR, exc_info=True) def _load_gpu_settings_from_settings(self): """GPU 관련 설정들을 SettingsManager에서 로드합니다.""" try: gpu_settings = [ 'use_cuda', 'migan_use_cuda', 'migan_intra_threads', 'migan_inter_threads', 'migan_use_tensorrt', 'migan_trt_fp16_enable', 'migan_max_image_size', 'force_cuda_cache_clear', 'migan_onnx_path', 'inpaint_method', 'min_masks_for_lama', 'use_roi_optimized_mask', 'enable_mask_refinement', 'context_expansion_ratio', 'blend_mode', 'performance_mode', 'max_image_size', 'roi_area_high', 'local_inpaint_method' ] for gpu_key in gpu_settings: if gpu_key in self.toggle_states: saved_value = self.settings_manager.get_value(f"gpu/{gpu_key}") if saved_value is not None: # 타입에 맞게 변환 original_value = self.toggle_states[gpu_key] if isinstance(original_value, bool): if isinstance(saved_value, str): self.toggle_states[gpu_key] = saved_value.lower() in ['true', '1', 'yes'] else: self.toggle_states[gpu_key] = bool(saved_value) elif isinstance(original_value, int): self.toggle_states[gpu_key] = int(saved_value) elif isinstance(original_value, float): self.toggle_states[gpu_key] = float(saved_value) else: self.toggle_states[gpu_key] = str(saved_value) self.logger.log(f"[GPU 설정 로드] {gpu_key}: {self.toggle_states[gpu_key]}", level=logging.DEBUG) self.logger.log("[SettingsManager] GPU 설정 로드 완료", level=logging.DEBUG) except Exception as e: self.logger.log(f"[SettingsManager] GPU 설정 로드 중 오류: {e}", level=logging.ERROR) def test_settings_sync(self): """설정 동기화 상태를 테스트하고 결과를 표시합니다.""" try: # 테스트 결과를 표시할 다이얼로그 생성 dialog = QDialog(self) dialog.setWindowTitle("설정 동기화 테스트 결과") dialog.setFixedSize(800, 600) layout = QVBoxLayout() # 결과 표시용 텍스트 위젯 result_text = QTextEdit() result_text.setReadOnly(True) layout.addWidget(result_text) # 닫기 버튼 close_button = QPushButton("닫기") close_button.clicked.connect(dialog.close) layout.addWidget(close_button) dialog.setLayout(layout) # 테스트 실행 result_lines = [] result_lines.append("=== 설정 동기화 테스트 결과 ===\n") # 1. toggle_states와 SettingsManager 동기화 확인 result_lines.append("1. toggle_states ↔ SettingsManager 동기화 확인:") sync_issues = [] for key, value in self.toggle_states.items(): # 저장된 값 확인 if isinstance(value, bool): saved_value = self.settings_manager.get_value(f"toggle/{key}") elif isinstance(value, str): saved_value = self.settings_manager.get_value(f"input/{key}") elif isinstance(value, (int, float)): saved_value = self.settings_manager.get_value(f"spinbox/{key}") else: saved_value = self.settings_manager.get_value(f"gpu/{key}") if saved_value is None: sync_issues.append(f" ❌ {key}: toggle_states={value}, 저장된값=없음") else: # 타입 변환 후 비교 try: if isinstance(value, bool): saved_bool = str(saved_value).lower() in ['true', '1', 'yes'] if isinstance(saved_value, str) else bool(saved_value) if value != saved_bool: sync_issues.append(f" ❌ {key}: toggle_states={value}, 저장된값={saved_bool}") elif isinstance(value, (int, float)): saved_num = type(value)(saved_value) if value != saved_num: sync_issues.append(f" ❌ {key}: toggle_states={value}, 저장된값={saved_num}") elif str(value) != str(saved_value): sync_issues.append(f" ❌ {key}: toggle_states='{value}', 저장된값='{saved_value}'") except (ValueError, TypeError) as e: sync_issues.append(f" ❌ {key}: 타입 변환 오류 - {e}") if sync_issues: result_lines.extend(sync_issues) else: result_lines.append(" ✅ 모든 설정이 동기화되어 있습니다.") # 2. widget_map과 UI 위젯 연결 확인 result_lines.append("\n2. widget_map ↔ UI 위젯 연결 확인:") widget_issues = [] for widget_name, config in self.widget_map.items(): widget = getattr(self, widget_name, None) if widget is None: widget_issues.append(f" ❌ {widget_name}: UI 위젯이 존재하지 않음") else: # 위젯 값과 저장된 값 비교 settings_key = config.get("key", widget_name) saved_value = self.settings_manager.get_value(settings_key) try: if hasattr(widget, "isChecked"): widget_value = widget.isChecked() if saved_value is not None: saved_bool = str(saved_value).lower() in ['true', '1', 'yes'] if isinstance(saved_value, str) else bool(saved_value) if widget_value != saved_bool: widget_issues.append(f" ❌ {widget_name}: UI={widget_value}, 저장된값={saved_bool}") elif hasattr(widget, "value"): widget_value = widget.value() if saved_value is not None and widget_value != type(widget_value)(saved_value): widget_issues.append(f" ❌ {widget_name}: UI={widget_value}, 저장된값={saved_value}") elif hasattr(widget, "text"): widget_value = widget.text() if saved_value is not None and widget_value != str(saved_value): widget_issues.append(f" ❌ {widget_name}: UI='{widget_value}', 저장된값='{saved_value}'") elif hasattr(widget, "currentText"): widget_value = widget.currentText() if saved_value is not None and widget_value != str(saved_value): widget_issues.append(f" ❌ {widget_name}: UI='{widget_value}', 저장된값='{saved_value}'") except Exception as e: widget_issues.append(f" ❌ {widget_name}: 비교 오류 - {e}") if widget_issues: result_lines.extend(widget_issues) else: result_lines.append(" ✅ 모든 위젯이 올바르게 연결되어 있습니다.") # 3. GPU 관련 설정 확인 result_lines.append("\n3. GPU 관련 설정 확인:") gpu_settings = [ 'use_cuda', 'migan_use_cuda', 'migan_intra_threads', 'migan_inter_threads', 'migan_use_tensorrt', 'migan_trt_fp16_enable', 'migan_max_image_size', 'force_cuda_cache_clear', 'migan_onnx_path', 'inpaint_method' ] gpu_issues = [] for gpu_key in gpu_settings: if gpu_key in self.toggle_states: toggle_value = self.toggle_states[gpu_key] saved_value = self.settings_manager.get_value(f"gpu/{gpu_key}") if saved_value is None: gpu_issues.append(f" ❌ {gpu_key}: toggle_states={toggle_value}, GPU설정=없음") else: try: if isinstance(toggle_value, bool): saved_bool = str(saved_value).lower() in ['true', '1', 'yes'] if isinstance(saved_value, str) else bool(saved_value) if toggle_value != saved_bool: gpu_issues.append(f" ❌ {gpu_key}: toggle_states={toggle_value}, GPU설정={saved_bool}") elif isinstance(toggle_value, (int, float)): saved_num = type(toggle_value)(saved_value) if toggle_value != saved_num: gpu_issues.append(f" ❌ {gpu_key}: toggle_states={toggle_value}, GPU설정={saved_num}") elif str(toggle_value) != str(saved_value): gpu_issues.append(f" ❌ {gpu_key}: toggle_states='{toggle_value}', GPU설정='{saved_value}'") except (ValueError, TypeError) as e: gpu_issues.append(f" ❌ {gpu_key}: 타입 변환 오류 - {e}") if gpu_issues: result_lines.extend(gpu_issues) else: result_lines.append(" ✅ 모든 GPU 설정이 올바르게 저장되어 있습니다.") # 4. 요약 total_issues = len(sync_issues) + len(widget_issues) + len(gpu_issues) result_lines.append(f"\n=== 요약 ===") result_lines.append(f"전체 검사 항목: {len(self.toggle_states) + len(self.widget_map) + len(gpu_settings)}개") result_lines.append(f"발견된 문제: {total_issues}개") if total_issues == 0: result_lines.append("🎉 모든 설정이 올바르게 동기화되어 있습니다!") else: result_lines.append("⚠️ 일부 설정에 동기화 문제가 있습니다.") result_lines.append("\n해결 방법:") result_lines.append("- 프로그램을 재시작해보세요") result_lines.append("- 설정을 다시 저장해보세요") result_lines.append("- 문제가 지속되면 관리자에게 문의하세요") # 결과를 텍스트 위젯에 표시 result_text.setPlainText('\n'.join(result_lines)) # 다이얼로그 표시 dialog.exec() self.logger.log(f"설정 동기화 테스트 완료. 발견된 문제: {total_issues}개", level=logging.INFO) except Exception as e: self.logger.log(f"설정 동기화 테스트 중 오류: {e}", level=logging.ERROR, exc_info=True) QMessageBox.critical(self, "오류", f"설정 동기화 테스트 중 오류가 발생했습니다:\n{e}") # def on_toggle_clicked_generic(self, key, is_checked): # """토글 클릭 시 상태 업데이트 및 저장""" # # 상태 업데이트하기 전에 이전 상태 저장 # prev_state = self.toggle_states.get(key, False) # # 상태 업데이트 # self.toggle_states[key] = is_checked # if is_checked: # status_text = "활성화" # else: # status_text = "비활성화" # label_text = "" # # key에 따라 라벨 텍스트를 설정 # if key == 'title': # label_text = self.title_toggle_label.text() # elif key == 'title_shuffle': # label_text = self.title_shuffle_toggle_label.text() # # 상품명 셔플이 켜지면 상품명 수정과 상품명 번역타입을 끄고 비활성화 # if is_checked: # self.title_toggle.setChecked(False) # self.title_trans_type_toggle.setChecked(False) # self.toggle_states['title'] = False # self.toggle_states['title_trans_type'] = False # else: # # 상품명 셔플이 꺼지면 상품명 수정과 상품명 번역타입을 다시 활성화 # self.title_toggle.setChecked(True) # self.title_trans_type_toggle.setChecked(True) # self.toggle_states['title'] = True # self.toggle_states['title_trans_type'] = True # elif key == 'title_trans_type': # label_text = self.title_trans_type_toggle_label.text() # elif key == 'optionTrnas': # label_text = self.optionTrnas_toggle_label.text() # elif key == 'optionIMGTrans': # label_text = self.optionIMGTrans_toggle_label.text() # elif key == 'optionIMGTrans_type': # label_text = self.optionIMGTrans_type_toggle_label.text() # elif key == 'optionAutoSelect': # label_text = self.optionAutoSelect_toggle_label.text() # elif key == 'price': # label_text = self.price_toggle_label.text() # elif key == 'thumb': # label_text = self.thumb_toggle_label.text() # elif key == 'thumb_trans_type': # label_text = self.thumb_trans_type_toggle_label.text() # elif key == 'thumb_nukki': # label_text = self.thumb_nukki_toggle_label.text() # # 썸네일 누끼 토글 상태에 따라 누끼 갯수 입력 필드 활성화/비활성화 # self.thumb_rmb_count_input.setEnabled(is_checked) # elif key == 'tag': # label_text = self.tag_toggle_label.text() # elif key == 'detail_Option': # label_text = self.detail_Option_toggle_label.text() # self.detail_text_button.setEnabled(is_checked) # elif key == 'detail_IMGTrans': # label_text = self.detail_IMGTrans_toggle_label.text() # # elif key == 'detail_IMGTans_type': # # label_text = self.detail_IMGTrans_type_toggle_label.text() # elif key == 'debug_mode': # label_text = self.debug_toggle_label.text() # elif key == 'discord': # label_text = self.discord_notify_toggle_label.text() # elif key == 'use_lens': # label_text = self.use_lens_toggle_label.text() # # use_lens가 켜지면 use_API를 끄기 (인터록) # if is_checked: # self.logger.log("렌즈 토글 켜짐", level=logging.DEBUG) # self.use_API_toggle.setChecked(False) # self.toggle_states['use_API'] = False # else: # self.logger.log("렌즈 토글 꺼짐", level=logging.DEBUG) # self.use_API_toggle.setChecked(True) # self.toggle_states['use_API'] = True # self.update_api_fields_visibility(not is_checked) # elif key == 'use_API': # label_text = self.use_API_toggle_label.text() # # use_API가 켜지면 use_lens를 끄기 (인터록) # if is_checked: # self.logger.log("API 토글 켜짐", level=logging.DEBUG) # self.use_lens_toggle.setChecked(False) # self.toggle_states['use_lens'] = False # else: # self.logger.log("API 토글 꺼짐", level=logging.DEBUG) # self.use_lens_toggle.setChecked(True) # self.toggle_states['use_lens'] = True # self.update_api_fields_visibility(is_checked) # elif key == 'watermark': # label_text = self.watermark_toggle_label.text() # elif key == 'ocr': # label_text = self.unwanted_words_button_label.text() # # OCR 토글 상태에 따라 불필요한 단어 설정 버튼 활성화/비활성화 # self.unwanted_words_button.setEnabled(is_checked) # # 사용자 등급이 Premium 이상인 경우에만 OCR 토글 활성화 # if not self.is_premium_or_higher(): # # OCR 활성화 시도 시 Premium 등급 체크 # self.toggle_states[key] = False # 상태를 다시 비활성화 # self.ocr_toggle.setChecked(False) # # self.ocr_toggle.setEnabled(False) # self.unwanted_words_button.setEnabled(False) # QMessageBox.warning(self, "권한 부족", "이미지 글자 인식 기능은 Premium 이상 등급에서만 사용 가능합니다.") # return # elif key == 'cat_rec': # # 카테 추천 토글 # label_text = self.cat_rec_toggle_label.text() # elif key == 'fixed_keywords' or key == 'keyword_fix': # # 키고정 토글 # label_text = self.keyword_fix_toggle_label.text() # # 키고정 토글 상태에 따라 개수 입력 필드 활성화/비활성화 # self.keyword_fix_count_input.setEnabled(is_checked) # # 키 이름이 'keyword_fix'로 들어온 경우 'fixed_keywords'로 저장 # if key == 'keyword_fix': # self.toggle_states['fixed_keywords'] = is_checked # elif key == 'remove_overprice': # # 가격초과제외 토글 # label_text = self.remove_overprice_toggle_label.text() # # 이미지 번역 관련 토글이 하나라도 켜져 있으면 워터마크 토글 보이기 # if key in ['optionIMGTrans', 'detail_IMGTrans', 'thumb']: # self.update_watermark_visibility() # # 워터마크 토글이 켜져 있으면 watermark_layout 보이기 # if key == 'watermark': # self.toggle_visibility(is_checked, [ # (self.watermark_text_input, self.watermark_text_label), # (self.opacity_percent_input, self.opacity_percent_label) # ]) # # # key에 따라 라벨 텍스트를 설정 # # if key == 'use_lens': # # self.on_lens_toggle_clicked(is_checked) # # discord 알림 토글 상태에 따라 webhook 입력 필드 확장 # if key == 'discord': # self.toggle_visibility(is_checked, [(self.webhook_input)]) # # key에 따라 라벨 텍스트를 설정 # self.logger.log(f"{label_text} 버튼 - {status_text} 선택", level=logging.DEBUG) # self.save_toggle_settings() def on_admin_toggle_clicked(self, is_checked): """관리자 토글 상태에 따라 관리자와 직원 필드를 표시/숨김""" if is_checked: # 관리자 모드: 직원 레이아웃을 숨기고, 관리자 PW를 표시 self.set_layout_visibility(self.admin_pw_layout, True) self.set_layout_visibility(self.user_id_layout, False) self.set_layout_visibility(self.user_pw_layout, False) else: # 직원 모드: 관리자 PW를 숨기고, 직원 레이아웃을 표시 self.set_layout_visibility(self.admin_pw_layout, False) self.set_layout_visibility(self.user_id_layout, True) self.set_layout_visibility(self.user_pw_layout, True) self.update_group_items(is_admin=is_checked) def on_collect_method_changed(self, index): value = self.collect_method_combo.currentData() # 예: "api" or "lens" # --- settings 저장: 반드시 widget_map의 key 사용 --- config = self.widget_map.get("collect_method_combo", {}) set_key = config.get("key", "collect_method_combo") # "global/collect_method"로 저장! self.settings_manager.save_value(set_key, value) # 설명 업데이트 if value == "lens": self.collect_method_widget.enterEvent = lambda e: self.show_manual_html( self.global_manual_group, "🛒 쇼핑렌즈 (VIP 전용)", self.global_manual_label, """

-상품의 대표이미지를 사용하여 네이버 쇼핑렌즈 검색을 실행하고,

검색결과를 이용해 상품명·카테고리·태그·가격등 상품정보를 수집.

판매량, 리뷰수, 평점을 고려한 최저가격 순으로 가져와

상품명가공, 가격, 카테고리, 태그 등 상품편집에 활용합니다.

이는 소싱이 잘못되었다 하더라도 쇼핑렌즈의 정보를 이용해

정확한 상품명과 카테고리로 자동으로 편집됩니다.

쇼핑렌즈 검색 결과는 최대 5개까지 가져올 수 있습니다.

-메모에 검색된 상품명과 가격을 기록, 상품검수에 활용가능
""" ) elif value == "api": self.collect_method_widget.enterEvent = lambda e: self.show_manual_html( self.global_manual_group, "🛒 쇼핑API", self.global_manual_label, """

-상품명을 이용해 네이버 쇼핑API방식으로 상품정보를 수집합니다.

수집되는 정보량은 적으나, 속도가 빠르고 캡차가 발생하지 않습니다.

가져온 정보는 상품명가공, 가격, 카테고리 등 상품편집에 활용합니다.

-쇼핑렌즈 검색 결과는 최대 5개까지 가져올 수 있습니다.

(현재는 두 값을 비워두시면 서버에서 API 처리되며 개인 API 키를 사용하실수 있습니다.)

-메모에 검색된 상품명과 가격을 기록, 상품검수에 활용가능
""" ) # --- 위젯맵에서 visible 정보 읽어와서 처리 --- value = self.collect_method_combo.currentData() # "api" or "lens" widget_map = self.widget_map.get("collect_method_combo", {}) # visible 관리 all_visible = set(sum(widget_map.get("visible", {}).values(), [])) for w in all_visible: widget = getattr(self, w, None) if widget: widget.setVisible(False) for w in widget_map.get("visible", {}).get(value, []): widget = getattr(self, w, None) if widget: widget.setVisible(True) # dependents 관리 all_dependents = set(sum(widget_map.get("dependents", {}).values(), [])) for w in all_dependents: widget = getattr(self, w, None) if widget: widget.setEnabled(False) for w in widget_map.get("dependents", {}).get(value, []): widget = getattr(self, w, None) if widget: widget.setEnabled(True) 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', 'src') # lib 디렉토리 포함 if os.path.exists(internal_dir): # lib 디렉토리가 존재하면 base_dir로 설정 return internal_dir else: # 일반 Python 실행 환경 base_dir = os.path.dirname(os.path.abspath(__file__)) debug_dir = os.path.join(base_dir, 'src') # lib 디렉토리 포함 return debug_dir def creat_Toggle_tab(self): """ 토글 설정을 탭 형식으로 생성하고 관리하는 메서드입니다. 기존 toggle_layout_widget 대신 사용됩니다. """ # 메인 위젯 및 레이아웃 생성 self.toggle_main_widget = QWidget() self.toggle_main_widget.setFixedHeight(450) self.toggle_main_widget.setFixedWidth(700) self.toggle_main_layout = QVBoxLayout(self.toggle_main_widget) self.toggle_main_layout.setContentsMargins(10, 10, 10, 10) self.toggle_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) # 등록상품모드 버튼 추가 if not hasattr(self, 'is_register_product_mode'): self.is_register_product_mode = False self.toggle_states['ed_mode'] = self.is_register_product_mode # (VIP 전용) 사업자관리 버튼 - 등록상품모드일 때만 표시 try: self.biz_manage_button = QPushButton('사업자관리', self) self.biz_manage_button.setToolTip('사업자 정보 및 마켓 계정 설정') self.biz_manage_button.clicked.connect(self.on_biz_manage_clicked) except Exception: self.biz_manage_button = None self.register_mode_button = QPushButton('등록상품모드', self) self.register_mode_button.setCheckable(True) self.register_mode_button.clicked.connect(self.on_register_mode_button_clicked) # 초기 상태 설정 self.register_mode_button.setChecked(False) self.register_mode_button.setStyleSheet(""" QPushButton { background-color: #f44336; color: white; border: 2px solid #da190b; border-radius: 8px; font-weight: bold; font-size: 12px; padding: 8px 16px; } QPushButton:hover { background-color: #da190b; } QPushButton:pressed { background-color: #c1170a; } """) title_layout.addStretch() # 왼쪽 정렬을 위한 스트레치 # VIP & 등록상품모드일 때만 사업자관리 버튼 표시 try: is_vip_or_admin = (self.user_membership_level or 'free').lower() in ['vip', 'admin'] if self.biz_manage_button is not None: # bizDBManager 미리 초기화 (UI 초기화 시점에서) if not hasattr(self, 'biz_dbManager') or self.biz_dbManager is None: try: self.biz_dbManager = BizDBManager(self.biz_db_path) except Exception: self.biz_dbManager = None # 라벨에 현재 선택된 마켓 개수 표시 label_suffix = self._get_current_biz_label_suffix() self.biz_manage_button.setText(f"사업자관리{label_suffix}") self.biz_manage_button.setVisible(is_vip_or_admin and self.toggle_states.get('ed_mode', False)) if self.biz_manage_button.isVisible(): title_layout.addWidget(self.biz_manage_button) else: # 자리만 보장 위해도 addWidget 필요; 가시성으로 제어 title_layout.addWidget(self.biz_manage_button) except Exception: pass title_layout.addWidget(self.register_mode_button) self.toggle_main_layout.addLayout(title_layout) # 탭 영역 (90%) self.toggle_tab_widget = QTabWidget() self.toggle_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_global_tab() self.create_product_name_tab() self.create_option_tab() self.create_tag_tab() self.create_price_tab() self.create_thumbnail_tab() self.create_detail_tab() self.create_etc_tab() self.create_upload_conditions_tab() # 업로드조건 탭 (등록모드 전용) self.toggle_main_layout.addWidget(self.toggle_tab_widget) return self.toggle_main_widget def on_register_mode_button_clicked(self, checked): """등록상품 모드 버튼 클릭 핸들러""" self.is_register_product_mode = bool(checked) self.toggle_states['ed_mode'] = self.is_register_product_mode # 버튼 텍스트/상태 동기화 self.register_mode_button.setChecked(self.is_register_product_mode) # 사업자관리 버튼 가시성 갱신 (VIP 전용) try: is_vip_or_admin = (self.user_membership_level or 'free').lower() in ['vip', 'admin'] if hasattr(self, 'biz_manage_button') and self.biz_manage_button is not None: label_suffix = self._get_current_biz_label_suffix() self.biz_manage_button.setText(f"사업자관리{label_suffix}") self.biz_manage_button.setVisible(is_vip_or_admin and self.is_register_product_mode) # 업로드정보삭제 버튼 및 Percenty 텍스트 갱신 if hasattr(self, 'remove_upload_info_button') and self.remove_upload_info_button is not None: self.remove_upload_info_button.setVisible(is_vip_or_admin and self.is_register_product_mode) if hasattr(self, 'market_change_button') and self.market_change_button is not None: self.market_change_button.setVisible(is_vip_or_admin and self.is_register_product_mode) if hasattr(self, 'upload_selected_markets_button') and self.upload_selected_markets_button is not None: self.upload_selected_markets_button.setVisible(is_vip_or_admin and self.is_register_product_mode) if hasattr(self, 'upload_selected_markets_Multi_button') and self.upload_selected_markets_Multi_button is not None: self.upload_selected_markets_Multi_button.setVisible(is_vip_or_admin and self.is_register_product_mode) # Percenty 버튼 텍스트 변경 if self.is_register_product_mode: self.PercentyJob_button.setText('현재그룹\n 상품편집 시작') else: self.PercentyJob_button.setText('상품편집\n시작') # 알바생 로그인 버튼 텍스트 및 스타일 변경 if hasattr(self, 'start_chrome_button') and self.start_chrome_button is not None: if self.is_register_product_mode: self.start_chrome_button.setText("업로드알바생\n로그인 [등록모드]") self.start_chrome_button.setStyleSheet(""" QPushButton { background: qlineargradient(x1:0, y1:0, x2:1, y2:1, stop:0 #ff6b35, stop:1 #f7931e); color: white; border: none; border-radius: 8px; font-size: 14px; font-weight: bold; padding: 8px; } QPushButton:hover { background: qlineargradient(x1:0, y1:0, x2:1, y2:1, stop:0 #e55a2b, stop:1 #e0861a); } QPushButton:pressed { background: qlineargradient(x1:0, y1:0, x2:1, y2:1, stop:0 #cc4d24, stop:1 #cc7916); } QPushButton:disabled { background: #cccccc; color: #666666; } """) else: self.start_chrome_button.setText("편집알바생\n로그인") # 기본 파란색 스타일로 복원 self.start_chrome_button.setStyleSheet(""" QPushButton { background: qlineargradient(x1:0, y1:0, x2:1, y2:1, stop:0 #2196f3, stop:1 #1976d2); color: white; border: none; border-radius: 8px; font-size: 14px; font-weight: bold; padding: 8px; } QPushButton:hover { background: qlineargradient(x1:0, y1:0, x2:1, y2:1, stop:0 #1976d2, stop:1 #1565c0); } QPushButton:pressed { background: qlineargradient(x1:0, y1:0, x2:1, y2:1, stop:0 #1565c0, stop:1 #0d47a1); } QPushButton:disabled { background: #cccccc; color: #666666; } """) except Exception: pass # 버튼 눌림 표시 (시각적 피드백) if self.is_register_product_mode: self.register_mode_button.setText("등록상품모드 ✓") self.register_mode_button.setStyleSheet(""" QPushButton { background-color: #4CAF50; color: white; border: 2px solid #45a049; border-radius: 8px; font-weight: bold; font-size: 12px; padding: 8px 16px; } QPushButton:hover { background-color: #45a049; } QPushButton:pressed { background-color: #3d8b40; } """) else: self.register_mode_button.setText("등록상품모드") self.register_mode_button.setStyleSheet(""" QPushButton { background-color: #f44336; color: white; border: 2px solid #da190b; border-radius: 8px; font-weight: bold; font-size: 12px; padding: 8px 16px; } QPushButton:hover { background-color: #da190b; } QPushButton:pressed { background-color: #c1170a; } """) # 등록모드 활성화 전제 조건 검증 및 안내 try: if self.is_register_product_mode: missing_msgs = [] admin_on = getattr(self, 'admin_toggle', None) if not admin_on or not self.admin_toggle.isChecked(): missing_msgs.append("관리자 모드 토글(ON)\n") if not self.admin_id_input.text().strip(): missing_msgs.append("관리자 ID\n") if not self.admin_pw_input.text().strip(): missing_msgs.append("관리자 PW\n") if missing_msgs: tip = "등록상품모드 사용 전 필요\n" + ", ".join(missing_msgs) self.register_mode_button.setToolTip(tip) QMessageBox.information(self, "등록상품모드 준비", tip + "\n설정을 완료한 뒤 다시 등록상품모드를 활성화해주세요.") # 조건 미충족 시 기본 상태로 복구 self.is_register_product_mode = False self.toggle_states['ed_mode'] = False self.register_mode_button.setChecked(False) # 버튼 표시/텍스트 원복 try: self.register_mode_button.setText("등록상품모드") self.register_mode_button.setStyleSheet(""" QPushButton { background-color: #f44336; color: white; border: 2px solid #da190b; border-radius: 8px; font-weight: bold; font-size: 12px; padding: 8px 16px; } QPushButton:hover { background-color: #da190b; } QPushButton:pressed { background-color: #c1170a; } """) # 사업자관리 버튼/업로드정보삭제 버튼 숨김 is_vip_or_admin = (self.user_membership_level or 'free').lower() in ['vip', 'admin'] if hasattr(self, 'biz_manage_button') and self.biz_manage_button is not None: label_suffix = self._get_current_biz_label_suffix() self.biz_manage_button.setText(f"사업자관리{label_suffix}") self.biz_manage_button.setVisible(False if is_vip_or_admin else False) if hasattr(self, 'remove_upload_info_button') and self.remove_upload_info_button is not None: self.remove_upload_info_button.setVisible(False) self.remove_upload_info_button.setEnabled(False) # 편집시작 버튼 텍스트 원복 self.PercentyJob_button.setText('상품편집\n시작') except Exception: pass self.apply_register_mode_tab_visibility() self.apply_register_mode_widget_visibility() except Exception: pass def on_biz_manage_clicked(self): """사업자관리 다이얼로그 오픈 (VIP 전용)""" try: # 지연 임포트로 의존성 최소화 from src.limited_contents.business_dialog import BizDialog if not hasattr(self, 'biz_dbManager') or self.biz_dbManager is None: # 기본 경로 사용 (내부에서 bizinfo.db 생성) self.biz_dbManager = BizDBManager(self.biz_db_path) self.max_biz_count = 20 dlg = BizDialog(biz_db_manager=self.biz_dbManager, max_biz_count=self.max_biz_count, parent=self, browser_controller=self.browser_controller) dlg.exec() # 버튼 라벨 갱신 (선택마켓 탭 사용으로 이름 표시는 유지) if hasattr(self, 'biz_manage_button') and self.biz_manage_button is not None: label_suffix = self._get_current_biz_label_suffix() self.biz_manage_button.setText(f"사업자관리{label_suffix}") # ed_mode & VIP이면 선택마켓 기준 정보 전달 is_ed_mode = self.toggle_states.get('ed_mode', False) membership = (self.user_membership_level or 'free').lower() is_vip_or_admin = membership in ['vip', 'admin'] if is_ed_mode and is_vip_or_admin: full_info = self.biz_dbManager.get_biz_full_info_legacy() self.browser_controller.update_biz_info(full_info) self.logger.log(f"선택마켓 정보 전달 완료", level=logging.DEBUG) except Exception as e: try: self.logger.log(f"사업자관리 다이얼로그 열기 실패: {e}", level=logging.ERROR, exc_info=True) except Exception: pass def on_remove_upload_info_clicked(self): """업로드정보삭제 버튼 클릭 핸들러 (VIP 전용)""" try: # VIP 권한 확인 membership = (self.user_membership_level or 'free').lower() is_vip_or_admin = membership in ['vip', 'admin'] if not is_vip_or_admin: self.logger.log("업로드정보삭제는 VIP 권한이 필요합니다.", level=logging.WARNING) QMessageBox.warning(self, "권한 없음", "업로드정보삭제는 VIP 권한이 필요합니다.") return # 확인 대화상자 reply = QMessageBox.question( self, "업로드정보삭제 확인", "업로드된 상품 정보를 삭제하시겠습니까?\n\n이 작업은 되돌릴 수 없습니다.", QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No, QMessageBox.StandardButton.No ) if reply == QMessageBox.StandardButton.No: return if reply == QMessageBox.StandardButton.Yes: # biz_dbManager 체크 if not hasattr(self, 'biz_dbManager') or self.biz_dbManager is None: self.logger.log("BizDBManager가 초기화되지 않았습니다.", level=logging.ERROR) QMessageBox.critical(self, "오류", "사업자 관리 모듈을 사용할 수 없습니다.") return # 선택마켓 정보 확인 work_combinations = self.biz_dbManager.get_biz_full_info() # 모든 작업순서의 마켓을 합쳐서 확인 all_markets = {} for work_combo in work_combinations: if isinstance(work_combo, dict) and 'markets' in work_combo: all_markets.update(work_combo['markets']) if not all_markets: self.logger.log("선택된 마켓 정보가 없습니다. 화면상의 모든 업로드 정보를 삭제합니다.", level=logging.INFO) else: self.logger.log(f"마켓정보전송 - {len(all_markets)}개 마켓", level=logging.INFO) # 올바른 선택마켓 구조로 브라우저 컨트롤러에 전달 legacy_info = self.biz_dbManager.get_biz_full_info_legacy() self.browser_controller.update_biz_info(legacy_info) # 실제 업로드정보 삭제 self.logger.log("업로드정보 삭제 요청", level=logging.INFO) self.browser_controller.start_RemoveMarketInfoJob_task() else: self.logger.log("업로드정보 삭제 취소", level=logging.INFO) except Exception as e: try: self.logger.log(f"업로드정보삭제 버튼 클릭 실패: {e}", level=logging.ERROR, exc_info=True) except Exception: pass def on_upload_selected_markets_clicked(self): """현재 그룹의 상품을 업로드합니다. (작업순서에 따라 순차 실행)""" try: # 등급별 권한 확인 membership = (self.user_membership_level or 'free').lower() # Basic, Premium, VIP 등급별 사업자 사용 가능 개수 allowed_business_count = { 'basic': 0, # Basic: 사업자정보 사용불가 'premium': 1, # Premium: 1개 사업자 정보 사용가능 'vip': 6, # VIP: 6개 사업자 정보 사용가능 'admin': 6 # Admin: VIP와 동일 } max_businesses = allowed_business_count.get(membership, 0) # 현재 선택된 그룹 확인 current_group = self.group_selector.currentText().strip() if not current_group or current_group == "전체": self.logger.log("전체 그룹은 업로드할 수 없습니다. 다른 그룹을 선택하세요.", level=logging.WARNING) QMessageBox.warning(self, "그룹 선택 오류", "전체 그룹은 업로드할 수 없습니다.\n다른 그룹을 선택하세요.") return # 현재 그룹에 상품이 있는지 확인 if self.total_product_count == 0: QMessageBox.warning(self, "오류", "상품이 없습니다. 다른 그룹을 선택하거나 해당그룹에 상품을 추가해주세요.") return # 사업자 정보 처리 (등급별) work_queue = [] if max_businesses > 0: # Premium/VIP만 사업자 정보 사용 가능 if not hasattr(self, 'biz_dbManager') or self.biz_dbManager is None: self.logger.log("사업자 정보가 없습니다. 먼저 사업자관리에서 설정하세요.", level=logging.WARNING) QMessageBox.warning(self, "사업자 정보 없음", "사업자 정보가 없습니다. 먼저 사업자관리에서 설정하세요.") return # 우선순위별 사업자 정보 가져오기 work_queue = self.get_work_queue_by_priority(max_businesses) if not work_queue: # 설정된 사업자가 없으면 Basic 모드(화면 설정값)로 진행할지 물어봄 reply = QMessageBox.question( self, "사업자 정보 없음", "설정된 사업자 정보가 없습니다.\n기존 설정(화면 설정값)대로 업로드를 진행하시겠습니까?", QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No, QMessageBox.StandardButton.No ) if reply == QMessageBox.StandardButton.No: return # 마켓 이름 매핑 market_names = { 'coupang': '쿠팡', 'ss': '스마트스토어', 'esm': 'ESM', '11st': '11번가(일반)', '11stg': '11번가(글로벌)', 'lotteon': '롯데온', 'ip': '인터파크', 'talkstore': '톡스토어' } # 확인 대화상자 내용 생성 if work_queue: # 다중 작업 (HTML 포맷) confirmation_text = f"""

🚀 순차 업로드 작업 확인

📦 현재 그룹: {current_group}

🔢 총 상품수: {self.total_product_count}개

👑 회원 등급: {membership.upper()}

📋 작업 순서 ({len(work_queue)}개)

""" for work_info in work_queue: order = work_info.get('order', 1) raw_markets = work_info.get('markets', []) # 마켓 이름 변환 및 스타일링 market_badges = [] for m in raw_markets: korean_name = market_names.get(m, m) market_badges.append(f"{korean_name}") markets_html = " ".join(market_badges) confirmation_text += f"""
[ {order}차 ] {markets_html}
""" confirmation_text += """

🔄 진행 과정:

  1. 마켓정보 자동 변경
  2. 등록상품 페이지 이동 & 그룹 선택
  3. 상품정보 수정 (옵션/가격 등)
  4. 기존 업로드 정보 삭제 (2차 작업부터)
  5. 상품 업로드

위 설정으로 업로드를 시작하시겠습니까?
(작업 시작 후에는 되돌릴 수 없습니다)

""" else: # 단일 작업 (Basic 등급 또는 사업자 정보 없음) confirmation_text = f"""

🚀 단일 업로드 작업 확인

📦 현재 그룹: {current_group}

🔢 총 상품수: {self.total_product_count}개

⚙️ 설정 모드: 기존 마켓 설정 유지

🔄 진행 과정:

  1. 등록상품 페이지 이동
  2. 그룹 선택
  3. 상품정보 수정
  4. 상품 업로드

업로드를 시작하시겠습니까?
(작업 시작 후에는 되돌릴 수 없습니다)

""" # 확인 대화상자 reply = QMessageBox.question( self, "현재 그룹의 상품을 업로드 확인", confirmation_text, QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No, QMessageBox.StandardButton.No ) if reply == QMessageBox.StandardButton.No: return # 업로드 프로세스 시작 (통합 프로세스 사용) self.start_multi_work_upload_process(current_group, work_queue) except Exception as e: try: self.logger.log(f"현재 그룹의 상품을 업로드 실패: {e}", level=logging.ERROR, exc_info=True) QMessageBox.critical(self, "오류", f"업로드 실행 중 오류가 발생했습니다:\n{str(e)}") except Exception: pass def on_upload_selected_markets_Multi_clicked(self): """(Deprecated) on_upload_selected_markets_clicked로 통합됨""" self.on_upload_selected_markets_clicked() def get_work_queue_by_priority(self, max_count): """작업순서별 마켓 조합 정보를 가져옵니다.""" try: work_queue = [] # 작업순서별 마켓 조합 정보 가져오기 (단순한 리스트) work_combinations = self.biz_dbManager.get_biz_full_info() if not work_combinations: self.logger.log("작업순서별 마켓 조합 정보가 없습니다.", level=logging.WARNING) return [] # max_count만큼만 처리 for work_combo in work_combinations[:max_count]: order = work_combo['order'] markets = work_combo['markets'] if not markets: continue # 이 작업의 사업자 정보 수집 (표시용) biz_names = [] market_list = [] for market_type, market_data in markets.items(): market_list.append(market_type) # 각 마켓의 사업자 이름 가져오기 (표시용) biz_id = market_data.get('biz_id') if biz_id: biz_info = self.biz_dbManager.get_biz(biz_id) if biz_info: biz_name = biz_info.get('name', biz_info.get('number', f'사업자{biz_id}')) biz_names.append(f"{market_type}:{biz_name}") # 작업명 생성 (표시용) work_name = f"{order}번 작업" if biz_names: work_name += f" ({', '.join(biz_names[:2])}{'...' if len(biz_names) > 2 else ''})" work_queue.append({ 'order': order, 'work_name': work_name, 'markets': market_list, 'market_info': markets, # 기존 호환성을 위한 필드들 'biz_id': None, # 작업 단위에는 단일 사업자 ID가 없음 'biz_name': work_name # 작업명을 사업자명으로 사용 }) self.logger.log(f"작업순서별 마켓 조합 큐 생성: {len(work_queue)}개", level=logging.INFO) for work in work_queue: markets_text = ", ".join(work['markets']) self.logger.log(f" {work['order']}번 작업: {markets_text}", level=logging.DEBUG) return work_queue except Exception as e: self.logger.log(f"작업 큐 생성 실패: {e}", level=logging.ERROR) return [] def start_multi_work_upload_process(self, group_name, work_queue): """다중 작업 업로드 프로세스를 시작합니다.""" try: # 진행상황 다이얼로그 생성 및 표시 from src.upload_progress_dialog import ModernProgressDialog self.upload_progress_dialog = ModernProgressDialog(self) # 다중 사업자 모드로 시작 if work_queue: total_products_per_business = {biz['biz_name']: self.total_product_count for biz in work_queue} self.upload_progress_dialog.start_multi_business_upload(work_queue, total_products_per_business) else: # 단일 모드 (Basic 등급) self.upload_progress_dialog.start_upload(self.total_product_count, {}) # 다이얼로그 시그널 연결 self.upload_progress_dialog.cancel_requested.connect(self.on_upload_cancel_requested) self.upload_progress_dialog.pause_requested.connect(self.on_upload_pause_requested) self.upload_progress_dialog.resume_requested.connect(self.on_upload_resume_requested) # 브라우저 컨트롤러 시그널은 이미 __init__에서 연결됨 (중복 연결 제거) # 비모달로 다이얼로그 표시 self.upload_progress_dialog.show() # 업로드 프로세스 상태 저장 self.current_upload_state = { 'group_name': group_name, 'work_queue': work_queue, 'current_work_index': 0, 'total_works': len(work_queue) if work_queue else 0, 'steps_completed': [] } # 첫 번째 단계 시작 self.execute_next_upload_step() except Exception as e: self.logger.log(f"다중 사업자 업로드 프로세스 시작 실패: {e}", level=logging.ERROR) QMessageBox.critical(self, "오류", f"업로드 프로세스 시작 중 오류가 발생했습니다:\n{str(e)}") def execute_next_upload_step(self): """다음 업로드 단계를 실행합니다.""" try: if not hasattr(self, 'current_upload_state'): self.logger.log("업로드 상태 정보가 없습니다.", level=logging.ERROR) return state = self.current_upload_state current_work_index = state['current_work_index'] work_queue = state['work_queue'] # 현재 사업자 정보 current_work = None if work_queue and current_work_index < len(work_queue): current_work = work_queue[current_work_index] # 진행률 계산 total_steps = 6 # 전체 단계 수 completed_steps = len(state['steps_completed']) business_progress = int((completed_steps / total_steps) * 100) if state['total_works'] > 0: overall_progress = int(((state['current_work_index'] * 100 + business_progress) / state['total_works'])) else: overall_progress = business_progress # 단계별 실행 if 'market_change' not in state['steps_completed']: # 1단계: 마켓정보 변경 (작업 정보가 있을 때만) if current_work: order = current_work.get('order', 1) work_name = current_work.get('work_name', f"{order}번 작업") markets = ", ".join(current_work.get('markets', [])) step_name = f"[{order}번 작업] 마켓정보 변경 중" self.upload_progress_dialog.update_progress(0, step_name, 10) self.upload_progress_dialog.add_log(f"{order}번 작업 마켓정보 변경 시작... ({markets})") # 새로운 구조: {'order': 1, 'markets': {...}} # 스마트스토어 백업 정보 찾기 (로그인 대응용) # 현재 작업에 스마트스토어가 없더라도, 로그인 팝업이 뜰 경우를 대비해 계정 정보를 함께 전달 backup_ss = None # 1. 현재 작업 큐의 다른 작업들에서 스마트스토어 정보 찾기 if work_queue: for w in work_queue: market_info = w.get('market_info', {}) if market_info and 'ss' in market_info: backup_ss = market_info['ss'] break # 2. 큐에도 없으면 DB에서 현재 1순위(또는 선택된) 스마트스토어 정보 조회 if not backup_ss: try: # 현재 선택된 마켓 정보(market_selection 테이블) 확인 selection = self.biz_dbManager.get_market_selection() ss_biz_id = selection.get('ss') if ss_biz_id: backup_ss = self.biz_dbManager.get_market_by_type(ss_biz_id, 'ss') except Exception as e: self.logger.log(f"백업 스마트스토어 정보 조회 실패: {e}", level=logging.WARNING) current_work_data = { 'order': current_work.get('order', 1), 'markets': current_work.get('market_info', {}), 'backup_ss': backup_ss } self.browser_controller.update_biz_info(current_work_data) self.browser_controller.start_ChangeBizJob_task() # 단계 완료는 시그널로 처리됨 else: # Basic 등급이므로 마켓정보 변경 건너뛰기 state['steps_completed'].append('market_change') self.execute_next_upload_step() elif 'go_to_registered' not in state['steps_completed']: # 2단계: 등록상품 페이지로 이동 self.upload_progress_dialog.update_progress(0, "등록상품 페이지로 이동 중", 20) self.upload_progress_dialog.add_log("등록상품 페이지로 이동 중...") self.browser_controller.go_to_registered_product_page_task() # 단계 완료는 시그널로 처리됨 elif 'select_group' not in state['steps_completed']: # 3단계: 그룹 선택 self.upload_progress_dialog.update_progress(0, f"그룹 '{state['group_name']}' 선택 중", 30) self.upload_progress_dialog.add_log(f"그룹 '{state['group_name']}' 선택 중...") self.browser_controller.select_group_task(group_name=state['group_name']) # 단계 완료는 시그널로 처리됨 elif 'product_edit' not in state['steps_completed']: # 4단계: 상품정보 수정 # 첫 번째 작업인 경우 상품정보 수정을 건너뜀 (이미 사용자가 편집했다고 가정) if current_work_index == 0: self.upload_progress_dialog.add_log("첫 번째 작업이므로 상품정보 수정 단계를 건너뜁니다.") state['steps_completed'].append('product_edit') self.execute_next_upload_step() else: self.upload_progress_dialog.update_progress(0, "상품정보 수정 중", 50) self.upload_progress_dialog.add_log("상품정보 수정 중...") self.start_product_edit_for_upload() # 단계 완료는 시그널로 처리됨 elif 'delete_upload_info' not in state['steps_completed']: # 5단계: 업로드정보 삭제 # 첫 번째 작업인 경우 업로드 정보 삭제 단계를 건너뜀 (신규 업로드로 간주) # 단, 사용자가 명시적으로 재업로드를 원할 수 있으므로, 재업로드 옵션이 있다면 확인 필요. # 여기서는 '순차적 다중 작업'의 특성상 첫 번째는 그냥 업로드, 두 번째부터는 삭제 후 업로드로 처리. if current_work_index == 0: self.upload_progress_dialog.add_log("첫 번째 작업이므로 업로드정보 삭제 단계를 건너뜁니다.") state['steps_completed'].append('delete_upload_info') self.execute_next_upload_step() else: self.upload_progress_dialog.update_progress(0, "업로드정보 삭제 중", 70) self.upload_progress_dialog.add_log("업로드정보 삭제 중...") self.browser_controller.start_RemoveMarketInfoJob_task() # 단계 완료는 시그널로 처리됨 elif 'upload_products' not in state['steps_completed']: # 6단계: 상품 업로드 self.upload_progress_dialog.update_progress(0, "상품 업로드 중", 90) self.upload_progress_dialog.add_log("상품 업로드 중...") self.browser_controller.start_UploadSelectedMarketsJob_task() # 단계 완료는 시그널로 처리됨 else: # 모든 단계 완료 - 다음 사업자 또는 전체 완료 self.proceed_to_next_work_or_complete() except Exception as e: self.logger.log(f"업로드 단계 실행 실패: {e}", level=logging.ERROR) self.upload_progress_dialog.add_log(f"오류 발생: {str(e)}") def proceed_to_next_work_or_complete(self): """다음 작업으로 진행하거나 전체 완료 처리""" try: state = self.current_upload_state state['current_work_index'] += 1 if state['current_work_index'] < state['total_works']: # 다음 작업으로 진행 next_work = state['work_queue'][state['current_work_index']] next_order = next_work.get('order', state['current_work_index'] + 1) next_markets = ", ".join(next_work.get('markets', [])) self.upload_progress_dialog.add_log(f"다음 {next_order}번 작업 처리 시작... ({next_markets})") # 단계 초기화 (마켓정보 변경부터 다시 시작) state['steps_completed'] = [] # 다이얼로그에서 다음 작업으로 전환 if hasattr(self.upload_progress_dialog, 'proceed_to_next_business'): self.upload_progress_dialog.proceed_to_next_business() # 다음 단계 실행 self.execute_next_upload_step() else: # 모든 작업 완료 self.upload_progress_dialog.add_log("모든 작업의 업로드가 완료되었습니다!") self.upload_progress_dialog.complete_upload() # 상태 정리 if hasattr(self, 'current_upload_state'): delattr(self, 'current_upload_state') except Exception as e: self.logger.log(f"다음 사업자 진행 실패: {e}", level=logging.ERROR) def start_product_edit_for_upload(self): """업로드를 위한 상품정보 수정을 시작합니다.""" try: # 기존 on_start_PercentyJob_clicked 로직 활용 # 하지만 UI 업데이트는 제외하고 실행 self.browser_controller.start_PercentyJob_task() except Exception as e: self.logger.log(f"상품정보 수정 시작 실패: {e}", level=logging.ERROR) def on_step_completed(self, step_name, success): """각 단계 완료 시 다음 단계로 진행""" try: self.logger.log(f"단계 '{step_name}' 완료: {'성공' if success else '실패'}", level=logging.INFO) if not hasattr(self, 'current_upload_state'): return state = self.current_upload_state if not success: # 단계 실패 시 오류 처리 self.upload_progress_dialog.add_log(f"단계 '{step_name}' 실행 중 오류가 발생했습니다.") error_msg = f"단계 '{step_name}' 실행 실패" self.upload_progress_dialog.complete_upload() self.upload_progress_dialog.add_log("업로드가 중단되었습니다.") # 상태 정리 if hasattr(self, 'current_upload_state'): delattr(self, 'current_upload_state') return # 성공한 경우 해당 단계를 완료 목록에 추가 if step_name not in state['steps_completed']: state['steps_completed'].append(step_name) # 다음 단계 실행 self.execute_next_upload_step() except Exception as e: self.logger.log(f"단계 완료 처리 실패: {e}", level=logging.ERROR) def on_upload_cancel_requested(self): """업로드 취소 요청 처리""" try: self.logger.log("사용자가 업로드 취소를 요청했습니다.", level=logging.INFO) # 브라우저 컨트롤러에 취소 신호 전달 if hasattr(self.browser_controller, 'cancel_upload'): self.browser_controller.cancel_upload() # 진행상황 다이얼로그 닫기 if hasattr(self, 'upload_progress_dialog') and self.upload_progress_dialog: self.upload_progress_dialog.add_log("업로드가 취소되었습니다.") self.upload_progress_dialog.accept() except Exception as e: self.logger.log(f"업로드 취소 처리 실패: {e}", level=logging.ERROR) def on_upload_pause_requested(self): """업로드 일시정지 요청 처리""" try: self.logger.log("사용자가 업로드 일시정지를 요청했습니다.", level=logging.INFO) # 브라우저 컨트롤러에 일시정지 신호 전달 if hasattr(self.browser_controller, 'pause_upload'): self.browser_controller.pause_upload() except Exception as e: self.logger.log(f"업로드 일시정지 처리 실패: {e}", level=logging.ERROR) def on_upload_resume_requested(self): """업로드 재개 요청 처리""" try: self.logger.log("사용자가 업로드 재개를 요청했습니다.", level=logging.INFO) # 브라우저 컨트롤러에 재개 신호 전달 if hasattr(self.browser_controller, 'resume_upload'): self.browser_controller.resume_upload() except Exception as e: self.logger.log(f"업로드 재개 처리 실패: {e}", level=logging.ERROR) def on_upload_progress_updated(self, current_product, total_products=None): """업로드 진행상황 업데이트 처리""" try: if hasattr(self, 'upload_progress_dialog') and self.upload_progress_dialog: # 총 상품수가 제공된 경우 업데이트 if total_products is not None and total_products != self.upload_progress_dialog.total_products: self.upload_progress_dialog.total_products = total_products self.upload_progress_dialog.overall_progress.setMaximum(total_products) self.logger.log(f"총 상품수가 {total_products}개로 업데이트되었습니다.", level=logging.INFO) # 현재 단계는 그대로 유지하고 상품 번호만 업데이트 current_step = self.upload_progress_dialog.current_step self.upload_progress_dialog.update_progress(current_product, current_step, 0) except Exception as e: self.logger.log(f"업로드 진행상황 업데이트 실패: {e}", level=logging.ERROR) def on_upload_step_changed(self, step_name, progress_percentage=0): """업로드 단계 변경 처리""" try: if hasattr(self, 'upload_progress_dialog') and self.upload_progress_dialog: current_product = self.upload_progress_dialog.current_product self.upload_progress_dialog.update_progress(current_product, step_name, progress_percentage) except Exception as e: self.logger.log(f"업로드 단계 변경 처리 실패: {e}", level=logging.ERROR) def on_upload_log_message(self, message): """업로드 로그 메시지 처리""" try: if hasattr(self, 'upload_progress_dialog') and self.upload_progress_dialog: self.upload_progress_dialog.add_log(message) except Exception as e: self.logger.log(f"업로드 로그 메시지 처리 실패: {e}", level=logging.ERROR) def on_upload_completed(self, success=True, message=""): """업로드 완료 처리""" try: self.logger.log(f"📢 on_upload_completed 호출됨 - 성공: {success}, 메시지: {message}", level=logging.INFO) # 진행률 다이얼로그가 있으면 업데이트 if hasattr(self, 'upload_progress_dialog') and self.upload_progress_dialog: if success: self.upload_progress_dialog.complete_upload() completion_message = message if message else "모든 작업이 성공적으로 완료되었습니다!" self.upload_progress_dialog.add_log(completion_message) else: error_message = f"업로드 중 오류 발생: {message}" self.upload_progress_dialog.add_log(error_message) # 오류 발생 시에도 닫기 버튼 활성화 self.upload_progress_dialog.pause_button.setEnabled(False) self.upload_progress_dialog.cancel_button.setEnabled(False) self.upload_progress_dialog.close_button.setEnabled(True) # 완료 알림 처리 # 성공 시에는 진행률 다이얼로그에 '완료' 상태가 표시되므로 별도의 팝업을 띄우지 않음 (사용자 요청) # 실패 시에만 경고 팝업 표시 if not success: error_message = f"업로드 중 오류 발생: {message}" if message else "업로드 중 오류가 발생했습니다." QTimer.singleShot(100, lambda: self.show_upload_completion_dialog(False, error_message)) else: # 성공 로그만 남김 self.logger.log("업로드 작업 정상 완료 (팝업 생략)", level=logging.INFO) except Exception as e: self.logger.log(f"업로드 완료 처리 실패: {e}", level=logging.ERROR) def show_upload_completion_dialog(self, success=True, message=""): """업로드 완료/실패 알림 다이얼로그 표시""" try: self.logger.log(f"🔔 show_upload_completion_dialog 호출됨 - 성공: {success}, 메시지: {message}", level=logging.INFO) # 다중 작업(순차 업로드) 진행 중인지 확인 is_multi_job = False is_last_job = True if hasattr(self, 'current_upload_state') and self.current_upload_state: is_multi_job = True current_idx = self.current_upload_state.get('current_work_index', 0) total_works = self.current_upload_state.get('total_works', 0) # 현재 작업이 마지막인지 확인 (다음 작업이 남아있으면 False) if current_idx < total_works - 1: is_last_job = False # 성공이고 다음 작업이 남았으면 자동 닫힘 메시지로 표시하고 종료 if success and is_multi_job and not is_last_job: self.show_auto_close_message("업로드 완료", f"{message}\n\n(다음 작업을 위해 2초 후 자동으로 닫힙니다)", 2000, 'information') return from PySide6.QtWidgets import QMessageBox from PySide6.QtCore import Qt msg_box = QMessageBox(self) msg_box.setWindowTitle("업로드 완료") if success: msg_box.setIcon(QMessageBox.Icon.Information) # 통계 정보 파싱 및 포맷팅 if "총 성공:" in message and "총 실패:" in message: # 업로드 작업 완료 - 총 성공: 0건, 총 실패: 3건 import re match = re.search(r'총 성공:\s*(\d+)건,\s*총 실패:\s*(\d+)건', message) if match: success_count = int(match.group(1)) fail_count = int(match.group(2)) total_count = success_count + fail_count title = "📤 업로드 완료!" detailed_msg = f""" 업로드 작업이 완료되었습니다. 📊 업로드 통계: • 총 처리 상품: {total_count}건 • ✅ 성공: {success_count}건 • ❌ 실패: {fail_count}건 성공률: {(success_count/total_count*100):.1f}% ({success_count}/{total_count}) """.strip() msg_box.setText(title) msg_box.setDetailedText(detailed_msg) else: msg_box.setText("📤 업로드 완료!") msg_box.setDetailedText(message) else: msg_box.setText("📤 업로드 완료!") msg_box.setDetailedText(message) else: msg_box.setIcon(QMessageBox.Icon.Critical) msg_box.setText("❌ 업로드 실패") msg_box.setDetailedText(message) # 버튼 설정 msg_box.setStandardButtons(QMessageBox.StandardButton.Ok) msg_box.setDefaultButton(QMessageBox.StandardButton.Ok) # 다이얼로그 스타일 설정 msg_box.setStyleSheet(""" QMessageBox { background-color: white; font-size: 12px; } QMessageBox QLabel { color: #2c3e50; font-weight: bold; } QPushButton { background-color: #3498db; color: white; border: none; padding: 8px 16px; border-radius: 4px; font-weight: bold; } QPushButton:hover { background-color: #2980b9; } """) # 다이얼로그 표시 msg_box.exec() self.logger.log(f"업로드 완료 알림 표시 - 성공: {success}, 메시지: {message}", level=logging.INFO) except Exception as e: self.logger.log(f"업로드 완료 다이얼로그 표시 실패: {e}", level=logging.ERROR) def on_market_change_button_clicked(self): """마켓정보변경 버튼 클릭 핸들러""" try: # 브라우저 컨트롤러가 있는지 확인 if not hasattr(self, 'browser_controller') or self.browser_controller is None: self.logger.log("브라우저 컨트롤러가 없습니다.", level=logging.WARNING) return # ed_mode와 VIP 권한 확인 is_ed_mode = self.toggle_states.get('ed_mode', False) membership = (self.user_membership_level or 'free').lower() is_vip_or_admin = membership in ['vip', 'admin'] if not (is_ed_mode and is_vip_or_admin): self.logger.log("마켓정보변경은 등록상품모드와 VIP 권한이 필요합니다.", level=logging.WARNING) return # 사업자 정보가 있는지 확인 if not hasattr(self, 'biz_dbManager') or self.biz_dbManager is None: self.logger.log("사업자 정보가 없습니다. 먼저 사업자관리에서 설정하세요.", level=logging.WARNING) return # 선택마켓 정보 확인 work_combinations = self.biz_dbManager.get_biz_full_info() # 모든 작업순서의 마켓을 합쳐서 확인 all_markets = {} for work_combo in work_combinations: if isinstance(work_combo, dict) and 'markets' in work_combo: all_markets.update(work_combo['markets']) if not all_markets: self.logger.log("선택된 마켓 정보가 없습니다. 먼저 사업자관리에서 마켓을 선택하세요.", level=logging.WARNING) return self.logger.log(f"마켓정보변경 시작 - {len(all_markets)}개 마켓", level=logging.INFO) # 올바른 선택마켓 구조로 브라우저 컨트롤러에 전달 legacy_info = self.biz_dbManager.get_biz_full_info_legacy() self.browser_controller.update_biz_info(legacy_info) # 마켓정보 변경 작업 시작 self.browser_controller.start_ChangeBizJob_task() except Exception as e: try: self.logger.log(f"마켓정보변경 버튼 클릭 실패: {e}", level=logging.ERROR, exc_info=True) except Exception: pass def show_auto_close_message(self, title, message, timeout=2000, type='information'): """일정 시간 후 자동으로 닫히는 메시지 박스 표시""" try: msg_box = QMessageBox(self) msg_box.setWindowTitle(title) msg_box.setText(message) if type == 'information': msg_box.setIcon(QMessageBox.Information) elif type == 'warning': msg_box.setIcon(QMessageBox.Warning) else: msg_box.setIcon(QMessageBox.Critical) # 타이머 설정 (timeout ms 후 닫힘) QTimer.singleShot(timeout, msg_box.close) msg_box.exec() except Exception: pass def on_market_change_completed(self, result_data): """마켓정보 변경 완료 시그널 핸들러""" try: # 변경결과보기 스위치가 켜져 있으면 결과 표시 if hasattr(self, 'biz_dbManager') and self.biz_dbManager and hasattr(self.biz_dbManager, 'show_result_enabled'): if self.biz_dbManager.show_result_enabled: # 다중 작업(순차 업로드) 진행 중인지 확인 is_multi_job = hasattr(self, 'current_upload_state') and self.current_upload_state is not None if is_multi_job: # 다중 작업 중이면 자동 닫힘 메시지 (2초) self.show_auto_close_message(result_data['title'], result_data['message'] + "\n\n(2초 후 자동으로 닫힙니다)", 2000, result_data['type']) else: # 단일 작업이면 일반 메시지 msg_box = QMessageBox(self) msg_box.setWindowTitle(result_data['title']) msg_box.setText(result_data['message']) # 메시지 타입에 따른 아이콘 설정 if result_data['type'] == 'information': msg_box.setIcon(QMessageBox.Information) elif result_data['type'] == 'warning': msg_box.setIcon(QMessageBox.Warning) else: # critical msg_box.setIcon(QMessageBox.Critical) msg_box.exec() except Exception as e: self.logger.log(f"마켓정보변경 결과 표시 실패: {e}", level=logging.ERROR) def _get_current_biz_label_suffix(self): """현재 활성화된 마켓 개수를 버튼 라벨에 표기하기 위한 헬퍼 형식: - 단일 작업: [3개선택] - 다중 작업: [1차:3, 2차:1] """ try: if not hasattr(self, 'biz_dbManager') or self.biz_dbManager is None: return "[0개선택]" # 1. 작업순서별 설정 확인 (market_priority 테이블) priorities = self.biz_dbManager.get_market_priorities() # 순서별 카운트 집계 # priorities 구조: {market_type: [biz_id1, biz_id2, ...]} order_counts = {} # 최대 차수 확인 max_order = 0 for biz_list in priorities.values(): max_order = max(max_order, len(biz_list)) # 차수별로 마켓 개수 세기 if max_order > 0: for i in range(max_order): count = 0 for mtype, biz_list in priorities.items(): # 해당 차수에 biz_id가 있으면 카운트 if i < len(biz_list) and biz_list[i] is not None: count += 1 if count > 0: order_counts[i + 1] = count # 2. 다중 작업인 경우 (2개 이상의 차수가 있거나, 순차적 표시 요구 시) if len(order_counts) > 1: # 예: [1차:3, 2차:1] parts = [] for order in sorted(order_counts.keys()): parts.append(f"{order}차:{order_counts[order]}") # 너무 길면 잘라서 표시 (예: 1차:3, 2차:1...) full_str = ", ".join(parts) if len(full_str) > 20: return f"[{full_str[:18]}...]" return f"[{full_str}]" elif len(order_counts) == 1: # 1개 차수만 있는 경우 -> 기존처럼 [N개선택] return f"[{order_counts[1]}개선택]" # 3. 데이터가 없는 경우 (fallback) # 기존 market_selection 테이블 확인 market_selection = self.biz_dbManager.get_market_selection() active_count = 0 market_types = ["coupang", "ss", "esm", "11st", "11stg", "lotteon", "talkstore"] for market_type in market_types: if market_selection.get(market_type): active_count += 1 return f"[{active_count}개선택]" except Exception: return "[0개선택]" def update_biz_manage_button_label(self): """사업자관리 버튼 라벨을 현재 마켓 선택 상태에 따라 업데이트 및 업로드 버튼들 가시성 설정""" try: # 사업자관리 버튼 라벨 업데이트 if hasattr(self, 'biz_manage_button') and self.biz_manage_button is not None: label_suffix = self._get_current_biz_label_suffix() self.biz_manage_button.setText(f"사업자관리{label_suffix}") self.logger.log(f"사업자관리 버튼 라벨 업데이트: 사업자관리{label_suffix}", level=logging.DEBUG) # VIP 및 등록상품모드 조건 확인 is_vip_or_admin = (self.user_membership_level or 'free').lower() in ['vip', 'admin'] is_register_mode = getattr(self, 'is_register_product_mode', False) # 업로드 관련 버튼들 가시성 설정 if hasattr(self, 'remove_upload_info_button') and self.remove_upload_info_button is not None: self.remove_upload_info_button.setVisible(is_vip_or_admin and is_register_mode) if hasattr(self, 'market_change_button') and self.market_change_button is not None: self.market_change_button.setVisible(is_vip_or_admin and is_register_mode) if hasattr(self, 'upload_selected_markets_button') and self.upload_selected_markets_button is not None: self.upload_selected_markets_button.setVisible(is_vip_or_admin and is_register_mode) if hasattr(self, 'upload_selected_markets_Multi_button') and self.upload_selected_markets_Multi_button is not None: self.upload_selected_markets_Multi_button.setVisible(is_vip_or_admin and is_register_mode) except Exception as e: self.logger.log(f"사업자관리 버튼 라벨 업데이트 실패: {e}", level=logging.ERROR) def _apply_initial_register_mode_visibility(self): """앱 최초 실행 시 기본모드로 가시성 규칙을 1회 강제 적용""" try: # 기본모드로 강제 초기화 self.is_register_product_mode = False self.toggle_states['ed_mode'] = self.is_register_product_mode if hasattr(self, 'register_mode_button') and self.register_mode_button is not None: self.register_mode_button.setChecked(False) self.register_mode_button.setText("등록상품모드") self.register_mode_button.setStyleSheet("") # 탭/위젯 가시성 동기화 self.apply_register_mode_tab_visibility() self.apply_register_mode_widget_visibility() # 알바생 로그인 버튼 초기 상태 설정 if hasattr(self, 'start_chrome_button') and self.start_chrome_button is not None: self.start_chrome_button.setText("편집알바생\n로그인") # 기본 파란색 스타일 적용 self.start_chrome_button.setStyleSheet(""" QPushButton { background: qlineargradient(x1:0, y1:0, x2:1, y2:1, stop:0 #2196f3, stop:1 #1976d2); color: white; border: none; border-radius: 8px; font-size: 14px; font-weight: bold; padding: 8px; } QPushButton:hover { background: qlineargradient(x1:0, y1:0, x2:1, y2:1, stop:0 #1976d2, stop:1 #1565c0); } QPushButton:pressed { background: qlineargradient(x1:0, y1:0, x2:1, y2:1, stop:0 #1565c0, stop:1 #0d47a1); } QPushButton:disabled { background: #cccccc; color: #666666; } """) except Exception as e: self.logger.log(f"_apply_initial_register_mode_visibility 오류: {e}", level=logging.WARNING) def apply_register_mode_tab_visibility(self): """등록상품 모드일 때 멤버십에 따른 탭 가시성 제어""" try: # 탭 인덱스 탐색 global_idx = self.toggle_tab_widget.indexOf(self.global_tab) name_idx = self.toggle_tab_widget.indexOf(self.product_name_tab) option_idx = self.toggle_tab_widget.indexOf(self.option_tab) tag_idx = self.toggle_tab_widget.indexOf(self.tag_tab) price_idx = self.toggle_tab_widget.indexOf(self.price_tab) thumb_idx = self.toggle_tab_widget.indexOf(self.thumbnail_tab) detail_idx = self.toggle_tab_widget.indexOf(self.detail_tab) etc_idx = self.toggle_tab_widget.indexOf(self.etc_tab) upload_conditions_idx = self.toggle_tab_widget.indexOf(self.upload_conditions_tab) if hasattr(self, 'upload_conditions_tab') else -1 # 기본: 모두 보임 show_global = show_name = show_option = show_tag = show_price = show_thumb = show_detail = show_etc = True show_upload_conditions = False # 업로드조건 탭은 기본적으로 숨김 if self.is_register_product_mode: # 등록상품모드: 등급별 노출 규칙 # basic → 상품명, 업로드조건 # premium→ 상품명, 옵션, 업로드조건 # vip/admin → 상품명, 옵션, 썸네일, 가격, 업로드조건 level = (self.user_membership_level or 'basic').lower() show_global = False show_tag = False show_detail = False show_etc = False show_upload_conditions = True # 등록모드에서는 항상 표시 if level == 'vip': show_name, show_option, show_thumb, show_price, show_detail = True, False, True, True, True elif level == 'premium': show_name, show_option, show_thumb, show_price, show_detail = True, False, False, True, False else: # 기타 미분류는 basic로 취급 show_name, show_option, show_thumb, show_price, show_detail = True, False, False, False, False self.logger.log(f"등록상품모드 활성화 - 등급:{level} → 탭 노출(상품명:{show_name}, 옵션:{show_option}, 썸네일:{show_thumb}, 가격:{show_price}, 상세페이지:{show_detail}, 업로드조건:{show_upload_conditions})", level=logging.INFO) else: self.logger.log("등록상품모드 비활성화 - 모든 탭 표시", level=logging.INFO) # 가시성 적용 (탭 이름 기반 제거/복원 방식) visibility_map = { "글로벌": show_global, "상품명": show_name, "옵션": show_option, "태그": show_tag, "가격": show_price, "썸네일": show_thumb, "상세페이지": show_detail, "기타 설정": show_etc, "업로드조건": show_upload_conditions, } self._apply_tab_visibility_map(visibility_map) # 현재 탭이 숨김 상태가 되지 않도록 보장 self._ensure_visible_tab(visibility_map) # 등급 표시는 더 이상 의미가 없어 'REGISTER_MODE'로 고정 표기 self.logger.log(f"등록상품모드 탭 가시성 적용 완료 - MODE: {'REGISTER_MODE' if self.is_register_product_mode else 'NORMAL'}, 글로벌:{show_global}, 상품명:{show_name}, 옵션:{show_option}, 태그:{show_tag}, 가격:{show_price}, 썸네일:{show_thumb}, 상세:{show_detail}, 기타:{show_etc}", level=logging.INFO) except Exception as e: self.logger.log(f"apply_register_mode_tab_visibility 오류: {e}", level=logging.WARNING) def _set_tab_visibility(self, tab_idx, visible, tab_name): """탭 가시성 설정 - 탭 제거/추가 방식""" try: if tab_idx == -1: self.logger.log(f"탭 '{tab_name}' 인덱스를 찾을 수 없습니다", level=logging.WARNING) return # 숨겨진 탭들을 저장할 딕셔너리 초기화 if not hasattr(self, '_hidden_tabs'): self._hidden_tabs = {} if not visible: # 탭을 제거하고 저장 if tab_name not in self._hidden_tabs: tab_widget = self.toggle_tab_widget.widget(tab_idx) if tab_widget: self.toggle_tab_widget.removeTab(tab_idx) self._hidden_tabs[tab_name] = tab_widget self.logger.log(f"탭 제거 성공 - {tab_name}", level=logging.INFO) else: self.logger.log(f"탭 위젯을 찾을 수 없음 - {tab_name}", level=logging.WARNING) else: self.logger.log(f"탭이 이미 숨겨져 있음 - {tab_name}", level=logging.DEBUG) else: # 숨겨진 탭을 복원 if tab_name in self._hidden_tabs: tab_widget = self._hidden_tabs[tab_name] # 적절한 위치에 탭 삽입 insert_idx = self._get_tab_insert_index(tab_name) self.toggle_tab_widget.insertTab(insert_idx, tab_widget, tab_name) del self._hidden_tabs[tab_name] self.logger.log(f"탭 복원 성공 - {tab_name} (위치: {insert_idx})", level=logging.INFO) else: self.logger.log(f"복원할 탭이 없음 - {tab_name}", level=logging.DEBUG) except Exception as e: self.logger.log(f"_set_tab_visibility 오류 - {tab_name}: {e}", level=logging.WARNING) def _get_tab_index_by_name(self, tab_name): try: count = self.toggle_tab_widget.count() for i in range(count): if self.toggle_tab_widget.tabText(i) == tab_name: return i return -1 except Exception: return -1 def _set_tab_visible_by_name(self, tab_name, visible): """이름으로 탭을 찾아 제거/복원합니다.""" try: if not hasattr(self, '_hidden_tabs'): self._hidden_tabs = {} current_idx = self._get_tab_index_by_name(tab_name) if not visible: # 이미 숨겨져 있으면 무시 if tab_name in self._hidden_tabs: return # 현재 보이는 탭이면 제거 후 저장 if current_idx != -1: tab_widget = self.toggle_tab_widget.widget(current_idx) if tab_widget is not None: self.toggle_tab_widget.removeTab(current_idx) self._hidden_tabs[tab_name] = tab_widget self.logger.log(f"탭 제거 성공 - {tab_name}", level=logging.DEBUG) return # visible=True: 필요 시 복원 if current_idx == -1 and tab_name in self._hidden_tabs: insert_idx = self._get_tab_insert_index(tab_name) self.toggle_tab_widget.insertTab(insert_idx, self._hidden_tabs[tab_name], tab_name) del self._hidden_tabs[tab_name] self.logger.log(f"탭 복원 성공 - {tab_name} (위치: {insert_idx})", level=logging.DEBUG) except Exception as e: self.logger.log(f"_set_tab_visible_by_name 오류 - {tab_name}: {e}", level=logging.WARNING) def _apply_tab_visibility_map(self, visibility_map): """여러 탭의 가시성을 한 번에 적용. 먼저 숨기고, 이후 보여줍니다.""" try: # 1) 숨길 탭들 먼저 제거 (인덱스 변동 영향 최소화) for tab_name, visible in visibility_map.items(): if not visible: self._set_tab_visible_by_name(tab_name, False) # 2) 보여줄 탭들 복원 for tab_name, visible in visibility_map.items(): if visible: self._set_tab_visible_by_name(tab_name, True) except Exception as e: self.logger.log(f"_apply_tab_visibility_map 오류: {e}", level=logging.WARNING) def _ensure_visible_tab(self, visibility_map): """현재 탭이 숨김이 된 경우, 가장 우선순위 높은 보이는 탭으로 전환.""" try: current_idx = self.toggle_tab_widget.currentIndex() if current_idx != -1: current_name = self.toggle_tab_widget.tabText(current_idx) if visibility_map.get(current_name, True): return # 현재 탭이 여전히 보이면 종료 # 우선순위 순서대로 첫 보이는 탭 선택 tab_order = ['글로벌', '상품명', '옵션', '태그', '가격', '썸네일', '상세페이지', '기타 설정'] for name in tab_order: if visibility_map.get(name, False): idx = self._get_tab_index_by_name(name) if idx != -1: self.toggle_tab_widget.setCurrentIndex(idx) return except Exception as e: self.logger.log(f"_ensure_visible_tab 오류: {e}", level=logging.WARNING) def _get_tab_insert_index(self, tab_name): """탭을 삽입할 적절한 인덱스 반환""" tab_order = ['글로벌', '상품명', '옵션', '태그', '가격', '썸네일', '상세페이지', '기타 설정'] try: target_idx = tab_order.index(tab_name) # 현재 표시된 탭들 중에서 적절한 위치 찾기 current_count = self.toggle_tab_widget.count() for i in range(current_count): current_tab_name = self.toggle_tab_widget.tabText(i) if current_tab_name in tab_order: current_idx = tab_order.index(current_tab_name) if current_idx > target_idx: return i return current_count # 맨 끝에 추가 except ValueError: return self.toggle_tab_widget.count() # 기본값: 맨 끝 def apply_register_mode_widget_visibility(self): """등록상품/기본 모드별로 위젯 맵 기반 가시성 제어""" try: # 등록모드일 때: 상품명 탭은 모두 표시, 옵션/썸네일 탭은 화이트리스트만 표시 if self.is_register_product_mode: option_container_layout = getattr(self, 'option_toggle_container_layout', None) if option_container_layout is None: option_container_layout = getattr(self, 'option_toggle_layout', None) if option_container_layout is not None: self._apply_whitelist_to_layout(option_container_layout, ['option_numbering_shuffle_widget']) if hasattr(self, 'thumbnail_toggle_layout') and self.thumbnail_toggle_layout is not None: self._apply_whitelist_to_layout(self.thumbnail_toggle_layout, ['thumb_represent_widget']) # 가격 탭: 등록모드에서는 '가격범위변경'만 표시 if hasattr(self, 'prices_toggle_layout') and self.prices_toggle_layout is not None: self._apply_whitelist_to_layout(self.prices_toggle_layout, ['price_range_widget']) if hasattr(self, 'detail_toggle_layout') and self.detail_toggle_layout is not None: # vip_detail_edit_widget 외에 detail_IMGTrans_widget(이미지번역), detail_concurrency_widget(동시설정), detail_promo_enabled_widget(홍보문구)도 허용 self._apply_whitelist_to_layout(self.detail_toggle_layout, ['vip_detail_edit_widget', 'detail_IMGTrans_widget', 'detail_concurrency_widget', 'detail_promo_enabled_widget']) else: # 기본모드: 옵션/썸네일 탭의 모든 위젯 표시 option_container_layout = getattr(self, 'option_toggle_container_layout', None) if option_container_layout is None: option_container_layout = getattr(self, 'option_toggle_layout', None) if option_container_layout is not None: self._show_all_in_layout(option_container_layout) if hasattr(self, 'thumbnail_toggle_layout') and self.thumbnail_toggle_layout is not None: self._show_all_in_layout(self.thumbnail_toggle_layout) # 기본모드: 가격 탭의 모든 위젯 표시 후, 등록모드 전용만 숨김 if hasattr(self, 'prices_toggle_layout') and self.prices_toggle_layout is not None: self._show_all_in_layout(self.prices_toggle_layout) self._hide_widgets_in_layout_by_names(self.prices_toggle_layout, ['price_range_widget']) if hasattr(self, 'detail_toggle_layout') and self.detail_toggle_layout is not None: self._show_all_in_layout(self.detail_toggle_layout) # VIP 상세페이지 수정 토글은 등록모드 + VIP 사용자에게만 표시 self._update_vip_detail_edit_visibility() # 추가적으로 맵 기반으로 특수 위젯 가시성 보정 (필요한 경우에만) widget_visibility_map = self._build_widget_visibility_map() if self.is_register_product_mode: # 등록모드에서만 표시할 위젯 for wname in widget_visibility_map.get('register', []): self._set_widget_visible(wname, True) # 항상 표시할 위젯 for wname in widget_visibility_map.get('both', []): self._set_widget_visible(wname, True) # 등록모드에서는 normal 전용 위젯 숨김 for wname in widget_visibility_map.get('normal', []): self._set_widget_visible(wname, False) else: # 기본모드에서만 표시할 위젯 + 항상 표시 for wname in widget_visibility_map.get('normal', []): self._set_widget_visible(wname, True) for wname in widget_visibility_map.get('both', []): self._set_widget_visible(wname, True) # 기본모드에서는 register 전용 위젯 숨김 for wname in widget_visibility_map.get('register', []): self._set_widget_visible(wname, False) except Exception as e: self.logger.log(f"apply_register_mode_widget_visibility 오류: {e}", level=logging.WARNING) def _set_widget_visible(self, widget_name, visible): try: if hasattr(self, widget_name): w = getattr(self, widget_name) if w is not None: w.setVisible(visible) except Exception as e: self.logger.log(f"_set_widget_visible 오류 - {widget_name}: {e}", level=logging.WARNING) def _apply_whitelist_to_layout(self, layout, allowed_widget_names): try: allowed_widgets = [] for name in allowed_widget_names: if hasattr(self, name): allowed_widgets.append(getattr(self, name)) for i in range(layout.count()): item = layout.itemAt(i) if item is None: continue widget = item.widget() if widget is not None: widget.setVisible(widget in allowed_widgets) except Exception as e: self.logger.log(f"_apply_whitelist_to_layout 오류: {e}", level=logging.WARNING) def _show_all_in_layout(self, layout): try: for i in range(layout.count()): item = layout.itemAt(i) if item is None: continue widget = item.widget() if widget is not None: widget.setVisible(True) except Exception as e: self.logger.log(f"_show_all_in_layout 오류: {e}", level=logging.WARNING) def _hide_widgets_in_layout_by_names(self, layout, widget_names): try: targets = [] for name in widget_names: if hasattr(self, name): targets.append(getattr(self, name)) for i in range(layout.count()): item = layout.itemAt(i) if item is None: continue widget = item.widget() if widget is not None and widget in targets: widget.setVisible(False) except Exception as e: self.logger.log(f"_hide_widgets_in_layout_by_names 오류: {e}", level=logging.WARNING) def _update_vip_detail_edit_visibility(self): """VIP 상세페이지 관련 토글의 가시성을 업데이트합니다""" try: is_vip = (self.user_membership_level or '').lower() == 'vip' # # VIP 상세페이지 수정 토글 (등록모드 + VIP 사용자만) # if hasattr(self, 'vip_detail_edit_widget'): # show_vip_edit_toggle = self.is_register_product_mode and is_vip # self.vip_detail_edit_widget.setVisible(show_vip_edit_toggle) # if show_vip_edit_toggle: # self.logger.log("VIP 상세페이지 수정 토글 표시 (등록모드 + VIP 사용자)", level=logging.DEBUG) # else: # if not self.is_register_product_mode: # self.logger.log("VIP 상세페이지 수정 토글 숨김 (기본모드)", level=logging.DEBUG) # elif not is_vip: # self.logger.log("VIP 상세페이지 수정 토글 숨김 (VIP 등급 아님)", level=logging.DEBUG) # 상페 이미지 번역 토글: 일반모드는 항상 표시, 등록모드는 VIP만 표시 if hasattr(self, 'detail_IMGTrans_widget'): # 일반모드(not 등록모드)이면 항상 표시, 등록모드이면 VIP만 표시 show_img_trans_toggle = (not self.is_register_product_mode) or is_vip self.detail_IMGTrans_widget.setVisible(show_img_trans_toggle) if show_img_trans_toggle: if not self.is_register_product_mode: self.logger.log("상페 이미지 번역 토글 표시 (일반모드)", level=logging.DEBUG) else: self.logger.log("상페 이미지 번역 토글 표시 (등록모드 + VIP 사용자)", level=logging.DEBUG) else: self.logger.log("상페 이미지 번역 토글 숨김 (등록모드 + VIP 등급 아님)", level=logging.DEBUG) except Exception as e: self.logger.log(f"_update_vip_detail_edit_visibility 오류: {e}", level=logging.WARNING) def _build_widget_visibility_map(self): """모드별 위젯 가시성 맵 구성 - register: 등록모드에서만 표시 - normal: 기본모드에서만 표시 - both: 양쪽 모두 표시 위젯 이름은 실제 self. 속성명과 일치해야 함. """ try: register_only = [ # 옵션 탭: 옵션명 셔플만 'option_numbering_shuffle_widget', # 썸네일 탭: 대표썸네일 변경만 'thumb_represent_widget', # 상세페이지 탭: VIP 상세페이지 수정 (VIP만) 'vip_detail_edit_widget', # 상세페이지 탭: 홍보문구 활성화 (등록모드 전용) 'detail_promo_enabled_widget', ] # 상품명 탭: 모두 표시 → 여기서는 known 위젯들을 both로 둠 product_name_widgets = [ 'title_widget', 'title_trans_type_widget', 'cat_rec_widget', # 필요 시 추가: 상품명 탭 내 위젯 객체명들 ] # 옵션 탭의 기타 위젯들 (등록모드에선 숨김, 기본모드에서는 표시 원하면 normal에 배치) option_other_widgets = [ 'option_name_toggle_widget', 'option_name_shuffle_widget', 'option_price_toggle_widget', 'option_price_shuffle_widget', 'option_img_toggle_widget', 'option_img_trans_widget', 'option_img_remove_bg_widget', 'option_img_inpaint_widget' ] # 썸네일 탭의 기타 위젯들 thumb_other_widgets = [ 'thumb_toggle_widget', 'thumb_trans_widget', 'thumb_remove_bg_widget', 'thumb_inpaint_widget', 'thumb_resize_widget', 'thumb_quality_widget' ] # 상세페이지 탭의 기타 위젯들 detail_other_widgets = [] both = [] + product_name_widgets # 동시 번역 설정 위젯을 both에 추가하여 항상 표시되도록 함 both.append('detail_concurrency_widget') normal_only = option_other_widgets + thumb_other_widgets return { 'register': register_only, 'normal': normal_only, 'both': both, } except Exception as e: self.logger.log(f"_build_widget_visibility_map 오류: {e}", level=logging.WARNING) return {'register': [], 'normal': [], 'both': []} def debug_tab_status(self): """탭 상태 디버깅용 함수""" try: if hasattr(self, 'toggle_tab_widget'): tab_count = self.toggle_tab_widget.count() self.logger.log(f"전체 탭 개수: {tab_count}", level=logging.INFO) # 탭 인덱스 상세 확인 tab_names = ['글로벌', '상품명', '옵션', '태그', '가격', '썸네일', '상세페이지', '기타 설정'] for name in tab_names: idx = -1 for i in range(tab_count): if self.toggle_tab_widget.tabText(i) == name: idx = i break self.logger.log(f"탭 '{name}' 인덱스: {idx}", level=logging.INFO) for i in range(tab_count): tab_text = self.toggle_tab_widget.tabText(i) is_visible = self.toggle_tab_widget.isTabVisible(i) self.logger.log(f"탭 {i}: '{tab_text}' - 가시성: {is_visible}", level=logging.INFO) else: self.logger.log("toggle_tab_widget이 존재하지 않습니다", level=logging.WARNING) except Exception as e: self.logger.log(f"debug_tab_status 오류: {e}", level=logging.WARNING) def toggle_descriptions(self, state): """ 설명 토글이 변경될 때 모든 설명의 가시성을 변경합니다. """ # state는 Qt.Checked(2) 또는 Qt.Unchecked(0) 값을 가짐 visible = state == Qt.Checked for description in self.findChildren(QLabel, "toggle_description"): description.setVisible(visible) self.logger.log(f"설명 표시 상태 변경: {'보이기' if visible else '숨기기'}", level=logging.DEBUG) 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 show_manual_html( self, titleGroupBox: QGroupBox, title: str, manual_label: QTextEdit, html_content: str, ): """매뉴얼 라벨에 HTML 내용을 표시합니다.""" titleGroupBox.setTitle(title) manual_label.setHtml(html_content) self._start_scroll_if_needed() # 스크롤 초기화 sb: QScrollBar = self.global_manual_label.verticalScrollBar() sb.setValue(sb.minimum()) self.current_cycle = 0 self.scroll_timer.stop() # 1초 뒤에 스크롤 시작 QTimer.singleShot(1000, self._start_scroll_if_needed) def reset_manual(self, titleGroupBox: QGroupBox, manual_label: QTextEdit): """manual_label을 기본 상태로 되돌립니다.""" self.show_manual_html( titleGroupBox, "매뉴얼", manual_label, "📝 기능 설명
" "기능별 스위치치에 마우스를 올리시면
" "자세한 설명이 표시됩니다." ) self.scroll_timer.stop() def get_icon_html(self, filename: str, width: int=20, height: int=20) -> str: """icons 폴더에서 이미지 경로를 찾아 HTML 태그를 생성합니다.""" path = os.path.join(self.icon_folder, filename) # 절대경로로 변환 후 file:// 스킴 사용 abs_path = os.path.abspath(path) return f'' def _start_scroll_if_needed(self): sb = self.global_manual_label.verticalScrollBar() if sb.maximum() > 0: self.scroll_timer.start() def _do_scroll_step(self): sb: QScrollBar = self.global_manual_label.verticalScrollBar() if sb.value() < sb.maximum(): sb.setValue(sb.value() + sb.singleStep()) else: self.current_cycle += 1 if self.current_cycle >= self.total_cycles: self.scroll_timer.stop() return sb.setValue(sb.minimum()) def create_global_tab(self): """글로벌 설정 탭을 생성합니다.""" self.global_tab = QWidget() self.global_layout = QHBoxLayout(self.global_tab) # 토글 버튼 그룹 self.global_toggle_group = QGroupBox("기능") self.global_toggle_layout = QVBoxLayout(self.global_toggle_group) # self.set_group_style(self.global_toggle_group, self.global_toggle_layout, "neumorphism") # self.global_toggle_layout.setContentsMargins(10, 10, 10, 10) # self.global_toggle_layout.setSpacing(10) # 설명 그룹 self.global_manual_group = QGroupBox("매뉴얼") self.global_manual_layout = QVBoxLayout(self.global_manual_group) self.set_group_style(self.global_manual_group, self.global_manual_layout, "modern") self.global_manual_layout.setContentsMargins(1, 1, 1, 1) self.global_manual_layout.setSpacing(1) self.global_manual_label = QTextEdit(self) self.global_manual_label.setReadOnly(True) # 읽기 전용으로 설정 self.global_manual_label.setWordWrapMode(QTextOption.NoWrap) self.global_manual_label.setAcceptRichText(True) # HTML 지원 self.global_manual_label.setLineWrapMode(QTextEdit.WidgetWidth) self.global_manual_label.setWordWrapMode(QTextOption.WrapAtWordBoundaryOrAnywhere) # ↓ 스크롤바 정책: 세로는 필요할 때, 가로는 항상 숨김 self.global_manual_label.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded) self.global_manual_label.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) self.global_manual_label.setStyleSheet(""" QTextEdit { background-color: white; border: 1px solid #e0e0e0; border-radius: 4px; padding: 8px; line-height: 1.4; color: #333; } """) self.reset_manual(self.global_manual_group, self.global_manual_label) self.global_manual_layout.addWidget(self.global_manual_label) # 토글 버튼 생성 # 상품정보 수집방법 콤보박스 (렌즈/쇼핑API) self.collect_method_widget = QWidget() self.collect_method_layout = QHBoxLayout(self.collect_method_widget) # self.use_API_client_info_layout = QVBoxLayout() # self.client_id_layout = QHBoxLayout() # self.client_secret_layout = QHBoxLayout() # self.use_API_client_info_layout.addLayout(self.client_id_layout) # self.use_API_client_info_layout.addLayout(self.client_secret_layout) self.collect_method_label = QLabel("상품정보수집방법", self) self.collect_method_label.setFixedHeight(30) self.collect_method_label.setFixedWidth(100) self.collect_method_combo = QComboBox(self) self.collect_method_combo.addItem("쇼핑API", "api") # VIP 전용: 쇼핑렌즈 옵션 (VIP 사용자만 표시) if self.is_vip_user(): self.collect_method_combo.addItem("쇼핑렌즈 (VIP)", "lens") self.collect_method_combo.setObjectName("collect_method_combo") self.collect_method_combo.setFixedHeight(30) #시그널 연결 self.collect_method_combo.currentIndexChanged.connect(self.on_collect_method_changed) self.collect_method_widget.leaveEvent = lambda e: self.reset_manual(self.global_manual_group, self.global_manual_label) # self.client_id_label = QLabel("Client ID", self) # self.client_id_input = QLineEdit(self) # self.client_id_input.setObjectName("client_id_input") # self.client_id_input.textChanged.connect(lambda text: self.universal_input_handler(self.client_id_input, text)) # self.client_id_input.setPlaceholderText("입력후엔터") # self.client_id_input.returnPressed.connect(self.update_client_info) # self.client_id_label.setVisible(False) # self.client_id_input.setVisible(False) # self.client_secret_label = QLabel("Client Secret", self) # self.client_secret_input = QLineEdit(self) # self.client_secret_input.setObjectName("client_secret_input") # self.client_secret_input.setPlaceholderText("입력후엔터") # self.client_secret_input.returnPressed.connect(self.update_client_info) # self.client_secret_input.textChanged.connect(lambda text: self.universal_input_handler(self.client_secret_input, text)) # self.client_secret_label.setVisible(False) # self.client_secret_input.setVisible(False) # self.client_id_layout.addWidget(self.client_id_label) # self.client_id_layout.addWidget(self.client_id_input) # self.client_secret_layout.addWidget(self.client_secret_label) # self.client_secret_layout.addWidget(self.client_secret_input) self.collect_method_layout.addWidget(self.collect_method_label) self.collect_method_layout.addWidget(self.collect_method_combo) # self.collect_method_layout.addLayout(self.use_API_client_info_layout) self.global_toggle_layout.addWidget(self.collect_method_widget) # 메모관련 UI 요소 초기화 self.memo_widget = QWidget() self.memo_toggle_layout = QHBoxLayout(self.memo_widget) self.memo_toggle_label = QLabel("메모입력", self) self.memo_toggle = ToggleSwitch(self) self.memo_toggle.setObjectName("memo_toggle") self.memo_toggle.clicked.connect(lambda checked: self.universal_input_handler(self.memo_toggle, checked)) self.memo_widget.enterEvent = lambda e: self.show_manual_html( self.global_manual_group, "📜 메모 입력 기능", self.global_manual_label, "상품정보 수집기능으로 수집한 정보를 메모란에 추가합니다.
" "'상품명 + 상품가' 로 기록되며 렌즈의 경우 노출순위 순서로,
" "API경우 순서는 노출순위와 상관없습니다.
" ) self.memo_widget.leaveEvent = lambda e: self.reset_manual(self.global_manual_group, self.global_manual_label) self.memo_toggle_layout.addWidget(self.memo_toggle_label) self.memo_toggle_layout.addWidget(self.memo_toggle) self.global_toggle_layout.addWidget(self.memo_widget, 2) # 메모관련 2번째 UI 요소 초기화 self.memo_widget2 = QWidget() self.memo_toggle_layout2 = QHBoxLayout(self.memo_widget2) self.memo_toggle_order_label = QLabel("메모순서", self) self.memo_toggle_order = ToggleSwitch(self) self.memo_toggle_order.setObjectName("memo_toggle_order") self.memo_toggle_order.setFixedWidth(100) self.memo_toggle_order.setOnText("사용자정보 먼저") self.memo_toggle_order.setOffText("상품정보 먼저") self.memo_toggle_order.clicked.connect(lambda checked: self.universal_input_handler(self.memo_toggle_order, checked)) self.memo_toggle_exposer_label = QLabel("메모노출", self) self.memo_toggle_exposer = ToggleSwitch(self) self.memo_toggle_exposer.setObjectName("memo_toggle_exposer") self.memo_toggle_exposer.clicked.connect(lambda checked: self.universal_input_handler(self.memo_toggle_exposer, checked)) self.memo_widget2.enterEvent = lambda e: self.show_manual_html( self.global_manual_group, "📜 메모 입력 옵션", self.global_manual_label, "메모 입력 옵션을 설정합니다.
" "메모순서 : 기존 사용자메모가 있을경우 어떤 메모를 먼저 사용할지 선택합니다.
" "메모노출 : 메모를 상품목록에 노출할지 여부를 선택합니다.
" "
" "메모 예시1)
" "사용자메모
" "----------
" "[수집방법:API]
" "상품명1 : 가격1
" "상품명2 : 가격2
" "
" "메모 예시2)
" "[수집방법:렌즈]
" "상품명1 : 가격1
" "상품명2 : 가격2
" "----------
" "사용자메모
" ) self.memo_widget2.leaveEvent = lambda e: self.reset_manual(self.global_manual_group, self.global_manual_label) self.memo_toggle_layout2.addWidget(self.memo_toggle_order_label) self.memo_toggle_layout2.addWidget(self.memo_toggle_order) self.memo_toggle_layout2.addWidget(self.memo_toggle_exposer_label) self.memo_toggle_layout2.addWidget(self.memo_toggle_exposer) self.global_toggle_layout.addWidget(self.memo_widget2) # OCR 관련 UI 요소 초기화 self.ocr_widget = QWidget() self.ocr_toggle_layout = QHBoxLayout(self.ocr_widget) self.ocr_toggle_label = QLabel("이미지글자인식", self) self.ocr_toggle = ToggleSwitch(self) self.ocr_toggle.setObjectName("ocr_toggle") # OCR 토글에 대한 독립적인 이벤트 핸들러 생성 self.ocr_toggle.clicked.connect(lambda checked: self.on_ocr_toggle_clicked(checked)) self.ocr_toggle.clicked.connect(lambda checked: self.universal_input_handler(self.ocr_toggle, checked)) self.ocr_widget.enterEvent = lambda e: self.show_manual_html( self.global_manual_group, "🔎 이미지 글자인식 기능 - [프리미엄 플랜 이상]", self.global_manual_label, "이미지에서 텍스트를 자동으로 인식하여 OCR 버튼으로
" "정의한 글자가 포함된 이미지를 제거합니다.
" "이미지 글자인식 기능은 상세페이지에서 사용됩니다.
" "예시: '免费换新(무료교환)' 이라는 단어가 설정된 경우
" "'免费换新' 단어가 포함된 이미지는 상세페이지에서 제거됩니다.
" ) self.ocr_widget.leaveEvent = lambda e: self.reset_manual(self.global_manual_group, self.global_manual_label) self.ocr_toggle_layout.addWidget(self.ocr_toggle_label) self.ocr_toggle_layout.addWidget(self.ocr_toggle) self.global_toggle_layout.addWidget(self.ocr_widget) # OCR 설정 버튼 self.ocr_setting_widget = QWidget() self.ocr_setting_layout = QHBoxLayout(self.ocr_setting_widget) self.unwanted_words_button_label = QLabel('OCR 설정', self) self.unwanted_words_button = QPushButton('OCR', self) self.unwanted_words_button.setObjectName("unwanted_words_button") # self.unwanted_words_button.setFixedWidth(30) self.set_PUSHBTN_style(self.unwanted_words_button,"modern") self.unwanted_words_button.clicked.connect(self.on_unwanted_words_button_clicked) self.unwanted_words_button.setEnabled(False) # 초기 상태는 비활성화 self.ocr_setting_widget.enterEvent = lambda e: self.show_manual_html( self.global_manual_group, "⚙️ OCR 단어 설정", self.global_manual_label, "OCR 기능에서 사용할 단어를 설정합니다.
" "한글로 입력하면 한자로 변환되어 함께 입력됩니다.
" " 예시: 무료교환(免费换新)
" "한자로 정확히 입력해야 정확한 결과를 얻을 수 있습니다.
" ) self.ocr_setting_widget.leaveEvent = lambda e: self.reset_manual(self.global_manual_group, self.global_manual_label) self.ocr_setting_layout.addWidget(self.unwanted_words_button_label) self.ocr_setting_layout.addWidget(self.unwanted_words_button) self.global_toggle_layout.addWidget(self.ocr_setting_widget) # self.global_toggle_layout.addLayout(self.ocr_setting_layout) # # 번역간 대기시간 # self.interval_widget = QWidget() # self.interval_spinbox_layout = QHBoxLayout(self.interval_widget) # self.interval_spinbox_label = QLabel("번역간격", self)# 작업 완료 메서드 수정 # self.interval_spinbox_label.setEnabled(False) # self.interval_spinbox = QSpinBox(self) # self.interval_spinbox.setEnabled(False) # # self.interval_spinbox.setFixedHeight(30) # self.interval_spinbox.setObjectName("interval") # self.interval_spinbox.setRange(2, 10) # self.interval_spinbox.setValue(3) # self.interval_spinbox.setSuffix("Second") # self.interval_spinbox.setFixedWidth(100) # # 인터벌 모드 토글에 대한 독립적인 이벤트 핸들러 생성 # self.interval_spinbox.valueChanged.connect(lambda value: self.universal_input_handler(self.interval_spinbox, value)) # self.interval_widget.enterEvent = lambda e: self.show_manual_html( # self.global_manual_group, # "번역 사이 간격 및 번역완료대기", # self.global_manual_label, # "번역간격
" # "번역간격 : 번역사이 시간간격을 설정합니다.
" # "최소 2초, 최대 10초까지 설정 가능합니다. 기본값은 3초입니다.
" # "설정된 시간의 50% ~ 200% 사이의 랜덤시간으로 휴식합니다.
" # "설정된 시간이 짧으면 서버에 많은 부담을 주어 매우매우 좋지않은 일이 일어날수 있습니다.
" # "번역완료대기
" # "대체번역시 번역이 완료될때까지 대기 시간을 설정합니다.
" # "최소 5초, 최대 60초까지 설정 가능합니다. 기본값은 20초입니다.
" # "설정된 시간이 길어도 번역이 끝나면 완료처리됩니다.
" # "그러나 서버에 부하가 몰리거나 번역속도가 느릴 경우 전체 번역 시간이 증가할 수 있습니다.
" # ) # self.interval_widget.leaveEvent = lambda e: self.reset_manual(self.global_manual_group, self.global_manual_label) # self.watingTime_spinbox_label = QLabel("번역대기", self)# 작업 완료 메서드 수정 # self.watingTime_spinbox_label.setEnabled(False) # self.watingTime_spinbox = QSpinBox(self) # self.watingTime_spinbox.setEnabled(False) # # self.watingTime_spinbox.setFixedHeight(30) # self.watingTime_spinbox.setObjectName("watingTime") # self.watingTime_spinbox.setRange(5, 60) # self.watingTime_spinbox.setValue(20) # self.watingTime_spinbox.setSuffix("Second") # self.watingTime_spinbox.setFixedWidth(100) # # 번역대기시간 스핀에 대한 독립적인 이벤트 핸들러 생성 # self.watingTime_spinbox.valueChanged.connect(lambda value: self.universal_input_handler(self.watingTime_spinbox, value)) # # self.watingTime_widget.enterEvent = lambda e: self.show_manual_html( # # self.global_manual_group, # # "번역시 대기기시간", # # self.global_manual_label, # # "대체번역시 번역이 완료될때까지 대기 시간을 설정합니다.
" # # "최소 5초, 최대 60초까지 설정 가능합니다. 기본값은 20초입니다.
" # # "설정된 시간이 길어도 번역이 끝나면 완료처리됩니다.
" # # "그러나 서버에 부하가 몰리거나 번역속도가 느릴 경우 전체 번역 시간이 증가할 수 있습니다.
" # # ) # # self.watingTime_widget.leaveEvent = lambda e: self.reset_manual(self.global_manual_group, self.global_manual_label) # self.interval_spinbox_layout.addWidget(self.interval_spinbox_label) # self.interval_spinbox_layout.addWidget(self.interval_spinbox) # self.interval_spinbox_layout.addWidget(self.watingTime_spinbox_label) # self.interval_spinbox_layout.addWidget(self.watingTime_spinbox) # self.global_toggle_layout.addWidget(self.interval_widget) # 이미지번역 폰트 타입 (버튼 + 라벨 방식) self.font_type_widget = QWidget() self.font_type_layout = QHBoxLayout(self.font_type_widget) self.font_type_label_title = QLabel("폰트 타입", self) # 폰트 선택 버튼 (현재 선택값 표시 포함) self.font_select_btn = QPushButton("선택없음 [폰트선택]") # self.font_select_btn.setFixedWidth(200) # 너비는 내용에 따라 유동적이거나 고정 self.font_select_btn.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) # 우측 확장 if hasattr(self, 'set_PUSHBTN_style'): self.set_PUSHBTN_style(self.font_select_btn, "modern") self.font_select_btn.clicked.connect(self.open_font_gallery) # 매뉴얼 설정 self.font_type_widget.enterEvent = lambda e: self.show_manual_html( self.global_manual_group, "🔄 폰트 타입", self.global_manual_label, """

이미지 번역에 사용할 폰트를 선택합니다.

버튼을 눌러 폰트 갤러리에서 미리보기를 확인하고 선택하세요.

""" ) self.font_type_widget.leaveEvent = lambda e: self.reset_manual(self.global_manual_group, self.global_manual_label) self.font_type_layout.addWidget(self.font_type_label_title) self.font_type_layout.addStretch() # 라벨과 버튼 사이 여백 (라벨 왼쪽, 버튼 오른쪽 배치 효과) self.font_type_layout.addWidget(self.font_select_btn) self.global_toggle_layout.addWidget(self.font_type_widget) # 디버그 모드 토글 self.debug_widget = QWidget() self.debug_toggle_layout = QHBoxLayout(self.debug_widget) self.debug_toggle_label = QLabel("디버그 모드", self)# 작업 완료 메서드 수정 self.debug_toggle = ToggleSwitch(self) self.debug_toggle.setObjectName("debug_toggle") # 디버그 토글에 대한 독립적인 이벤트 핸들러 생성 self.debug_toggle.clicked.connect(lambda checked: self.universal_input_handler(self.debug_toggle, checked)) self.debug_widget.enterEvent = lambda e: self.show_manual_html( self.global_manual_group, "🐞 디버그 모드", self.global_manual_label, "문제 발생 시 내부 로그를 확인하며 직접 디버깅할 수 있습니다." ) self.debug_widget.leaveEvent = lambda e: self.reset_manual(self.global_manual_group, self.global_manual_label) self.debug_toggle_layout.addWidget(self.debug_toggle_label) self.debug_toggle_layout.addWidget(self.debug_toggle) self.global_toggle_layout.addWidget(self.debug_widget) # 레이아웃에 그룹 추가 (고정 비율 6:4) self.global_layout.addWidget(self.global_toggle_group, 6) self.global_layout.addWidget(self.global_manual_group, 4) # 레이아웃 추가 self.toggle_tab_widget.addTab(self.global_tab, "글로벌") return self.global_tab def create_product_name_tab(self): """상품명 탭을 생성합니다.""" self.product_name_tab = QWidget() self.product_name_layout = QHBoxLayout(self.product_name_tab) # 토글 버튼 그룹 self.product_name_toggle_group = QGroupBox("기능") # 스크롤 영역으로 감싸, 토글이 많아져도 공간 확보 self.product_name_toggle_layout = QVBoxLayout(self.product_name_toggle_group) try: self.product_name_scroll = QScrollArea(self.product_name_toggle_group) self.product_name_scroll.setWidgetResizable(True) self.product_name_scroll.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) self.product_name_toggle_container = QWidget() self.product_name_toggle_container_layout = QVBoxLayout(self.product_name_toggle_container) except Exception: self.product_name_scroll = None self.set_group_style(self.product_name_toggle_group, self.product_name_toggle_layout, "neumorphism") self.product_name_toggle_layout.setSpacing(14) # 설명 그룹 self.product_name_manual_group = QGroupBox("매뉴얼") self.product_name_manual_layout = QVBoxLayout(self.product_name_manual_group) self.set_group_style(self.product_name_manual_group, self.product_name_manual_layout, "modern") self.product_name_manual_layout.setContentsMargins(1, 1, 1, 1) self.product_name_manual_layout.setSpacing(1) self.product_name_manual_label = QTextEdit(self) self.product_name_manual_label.setReadOnly(True) # 읽기 전용으로 설정 self.product_name_manual_label.setWordWrapMode(QTextOption.NoWrap) self.product_name_manual_label.setAcceptRichText(True) # HTML 지원 self.product_name_manual_label.setLineWrapMode(QTextEdit.WidgetWidth) self.product_name_manual_label.setWordWrapMode(QTextOption.WrapAtWordBoundaryOrAnywhere) # ↓ 스크롤바 정책: 세로는 필요할 때, 가로는 항상 숨김 self.product_name_manual_label.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded) self.product_name_manual_label.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) self.product_name_manual_label.setStyleSheet(""" QTextEdit { background-color: white; border: 1px solid #e0e0e0; border-radius: 4px; padding: 8px; line-height: 1.4; color: #333; } """) self.reset_manual(self.product_name_manual_group, self.product_name_manual_label) self.product_name_manual_layout.addWidget(self.product_name_manual_label) # 토글 버튼 생성 # 상품명 수정 토글 self.title_widget = QWidget() self.title_toggle_layout = QHBoxLayout(self.title_widget) self.title_toggle_label = QLabel("AI 상품명 생성", self) self.title_toggle = ToggleSwitch(self) self.title_toggle.setObjectName("title_toggle") self.title_toggle.clicked.connect(lambda checked: self.universal_input_handler(self.title_toggle, checked)) self.title_toggle.clicked.connect(self.on_title_toggle_for_interlock) self.title_widget.enterEvent = lambda e: self.show_manual_html( self.product_name_manual_group, "✏️ AI 상품명 생성", self.product_name_manual_label, """

상품정보수집 결과 스스에서 가장 잘 팔린 상품들의 상품명을 가공하여,

금지어, 브랜드명 제거, 중복 제거, 중요 키워드 보존 등을 진행합니다.

원본상품명과 옵션명을 활용하여 실제 소싱된 상품의 특징을 반영하고,

이를 통해 각 마켓의 로직에 맞춘 최적의 상품명을 AI가 만들어 줍니다.

수집된 상품명의 키 고정을 지원하여 소싱키워드를 보존할 수 있습니다.

상품정보수집 결과를 사용하지 않을 경우, 소싱된 상품명을 AI를 이용해 가공합니다.

상품명 가공 시 네이버 SEO 최적화를 지원합니다.

상품명이 중복될 경우 저장오류로 인해 중복상품명 팝업이 뜨며, 재생성 됩니다.

최종오류시 최종오류상품으로 저장됩니다.

""" ) self.title_widget.leaveEvent = lambda e: self.reset_manual(self.product_name_manual_group, self.product_name_manual_label) self.title_toggle_layout.addWidget(self.title_toggle_label) self.title_toggle_layout.addWidget(self.title_toggle) target_layout = getattr(self, 'product_name_toggle_container_layout', self.product_name_toggle_layout) target_layout.addWidget(self.title_widget) # 상품명 번역 타입 토글 self.title_trans_type_widget = QWidget() self.title_trans_type_toggle_layout = QHBoxLayout(self.title_trans_type_widget) self.title_trans_type_toggle_label = QLabel("상품명 번역 타입", self) self.title_trans_type_toggle = ToggleSwitch(self) self.title_trans_type_toggle.setObjectName("title_trans_type_toggle") self.title_trans_type_toggle.clicked.connect(lambda checked: self.universal_input_handler(self.title_trans_type_toggle, checked)) self.title_trans_type_toggle.setOnText("구글") self.title_trans_type_toggle.setOffText("GPT") self.title_trans_type_widget.enterEvent = lambda e: self.show_manual_html( self.product_name_manual_group, "🔄 상품명 번역 타입", self.product_name_manual_label, """

원본상품명 번역에 사용되는 번역기를 선택합니다.

구글: 빠른 번역 속도와 안정적인 결과를 제공합니다.

GPT: 더 자연스러운 번역과 문맥 이해가 필요할 때 사용합니다.

일반적인 상품은 구글 번역을, 복잡한 설명이 필요한 상품은 GPT를 권장합니다.

""" ) self.title_trans_type_widget.leaveEvent = lambda e: self.reset_manual(self.product_name_manual_group, self.product_name_manual_label) self.title_trans_type_toggle_layout.addWidget(self.title_trans_type_toggle_label) self.title_trans_type_toggle_layout.addWidget(self.title_trans_type_toggle) target_layout.addWidget(self.title_trans_type_widget) # 카테 추천 토글 self.cat_rec_widget = QWidget() self.cat_rec_toggle_layout = QHBoxLayout(self.cat_rec_widget) self.cat_rec_toggle_label = QLabel("카테 추천", self) self.cat_rec_toggle = ToggleSwitch(self) self.cat_rec_toggle.setObjectName("cat_rec_toggle") self.cat_rec_toggle.clicked.connect(lambda checked: self.universal_input_handler(self.cat_rec_toggle, checked)) self.cat_rec_widget.enterEvent = lambda e: self.show_manual_html( self.product_name_manual_group, "📁 카테고리 추천", self.product_name_manual_label, """

기본적으로 쇼핑렌즈 검색 결과를 참고하여 팔린 상품의 카테고리가 설정되나,

상품명 탭의 카테고리 추천버튼을 누르길 원할때 사용합니다.

올바른 카테고리 선택은 검색 노출과 고객 타겟팅에 중요합니다.

""" ) self.cat_rec_widget.leaveEvent = lambda e: self.reset_manual(self.product_name_manual_group, self.product_name_manual_label) self.cat_rec_toggle_layout.addWidget(self.cat_rec_toggle_label) self.cat_rec_toggle_layout.addWidget(self.cat_rec_toggle) target_layout.addWidget(self.cat_rec_widget) # 상품명 셔플 토글 self.title_shuffle_widget = QWidget() self.title_shuffle_toggle_layout = QHBoxLayout(self.title_shuffle_widget) self.title_shuffle_toggle_label = QLabel("상품명 셔플", self) self.title_shuffle_toggle = ToggleSwitch(self) self.title_shuffle_toggle.setObjectName("title_shuffle_toggle") self.title_shuffle_toggle.clicked.connect(lambda checked: self.universal_input_handler(self.title_shuffle_toggle, checked)) self.title_shuffle_toggle.clicked.connect(self.on_title_shuffle_toggle_for_interlock) self.title_shuffle_widget.enterEvent = lambda e: self.show_manual_html( self.product_name_manual_group, "🔀 상품명 셔플", self.product_name_manual_label, """

소싱 상품명을 키고정 후 셔플(랜덤섞기)합니다.

소싱된 상품명을 유지하고 싶거나, 타 사업자로 업로드시 활용할수 있습니다.

중복단어 허용은 최대 1개까지 입니다.

""" ) self.title_shuffle_widget.leaveEvent = lambda e: self.reset_manual(self.product_name_manual_group, self.product_name_manual_label) self.title_shuffle_toggle_layout.addWidget(self.title_shuffle_toggle_label) self.title_shuffle_toggle_layout.addWidget(self.title_shuffle_toggle) target_layout.addWidget(self.title_shuffle_widget) # 키고정 토글 self.keyword_fix_widget = QWidget() self.keyword_fix_toggle_layout = QHBoxLayout(self.keyword_fix_widget) self.keyword_fix_toggle_label = QLabel("키고정", self) self.keyword_fix_toggle = ToggleSwitch(self) 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_widget.enterEvent = lambda e: self.show_manual_html( self.product_name_manual_group, "🔑 키워드 고정", self.product_name_manual_label, """

소싱상품명의 중요 키워드를 보존하여 상품명 가공시 활용합니다.

스스의 노출로직에 맞추어 키워드를 고정할 수 있습니다.

검색 최적화를 위해 중요한 기능입니다.

키고정 수를 통해 몇 개의 키워드를 고정할지 설정할 수 있습니다.

""" ) self.keyword_fix_widget.leaveEvent = lambda e: self.reset_manual(self.product_name_manual_group, self.product_name_manual_label) self.keyword_fix_toggle_layout.addWidget(self.keyword_fix_toggle_label) self.keyword_fix_toggle_layout.addWidget(self.keyword_fix_toggle) target_layout.addWidget(self.keyword_fix_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_input = QSpinBox(self) 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_fix_count_input.setEnabled(False) self.keyword_count_widget.enterEvent = lambda e: self.show_manual_html( self.product_name_manual_group, "🔢 키고정 수 설정", self.product_name_manual_label, """

상품명에서 고정할 키워드의 개수를 설정합니다.

1에서 8까지의 값을 설정할 수 있습니다.

너무 많은 키워드를 고정하면 번역의 자연스러움이 떨어질 수 있습니다.

일반적으로 2~4개의 키워드 고정을 권장합니다.

""" ) self.keyword_count_widget.leaveEvent = lambda e: self.reset_manual(self.product_name_manual_group, self.product_name_manual_label) self.keyword_count_layout.addWidget(self.keyword_fix_count_label) self.keyword_count_layout.addWidget(self.keyword_fix_count_input) target_layout.addWidget(self.keyword_count_widget) # 포함 단어 삭제 토글 self.sub_word_remove_widget = QWidget() self.sub_word_remove_layout = QHBoxLayout(self.sub_word_remove_widget) self.sub_word_remove_label = QLabel("포함 단어 삭제", self) self.sub_word_remove_toggle = ToggleSwitch(self) self.sub_word_remove_toggle.setObjectName("sub_word_remove_toggle") self.sub_word_remove_toggle.clicked.connect(lambda checked: self.universal_input_handler(self.sub_word_remove_toggle, checked)) self.sub_word_remove_widget.enterEvent = lambda e: self.show_manual_html( self.product_name_manual_group, "✂️ 포함 단어 삭제", self.product_name_manual_label, """

상품명 생성 시, 다른 단어에 포함되는 짧은 단어를 삭제합니다.

예: '식탁의자', '의자'가 있을 때 '의자'를 삭제합니다.

중복된 의미를 줄여 상품명을 간결하게 만듭니다.

""" ) self.sub_word_remove_widget.leaveEvent = lambda e: self.reset_manual(self.product_name_manual_group, self.product_name_manual_label) self.sub_word_remove_layout.addWidget(self.sub_word_remove_label) self.sub_word_remove_layout.addWidget(self.sub_word_remove_toggle) target_layout.addWidget(self.sub_word_remove_widget) # 경고 단어 삭제 토글 self.del_warning_word_widget = QWidget() self.del_warning_word_layout = QHBoxLayout(self.del_warning_word_widget) self.del_warning_word_label = QLabel("경고 단어 삭제", self) self.del_warning_word_toggle = ToggleSwitch(self) self.del_warning_word_toggle.setObjectName("del_warning_word_toggle") self.del_warning_word_toggle.clicked.connect(lambda checked: self.universal_input_handler(self.del_warning_word_toggle, checked)) self.del_warning_word_widget.enterEvent = lambda e: self.show_manual_html( self.product_name_manual_group, "⚠️ 경고 단어 삭제", self.product_name_manual_label, """

상품명 설정 후 '경고단어 삭제하기' 버튼이 나타나면 자동으로 클릭합니다.

""" ) self.del_warning_word_widget.leaveEvent = lambda e: self.reset_manual(self.product_name_manual_group, self.product_name_manual_label) self.del_warning_word_layout.addWidget(self.del_warning_word_label) self.del_warning_word_layout.addWidget(self.del_warning_word_toggle) target_layout.addWidget(self.del_warning_word_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)) self.title_length_limit_widget.enterEvent = lambda e: self.show_manual_html( self.product_name_manual_group, "🔢 상품명 최대 길이 설정", self.product_name_manual_label, """

상품명 작성시 최대 길이를 설정합니다. (AI작성, 셔플 포함)

10~50자 사이의 값을 설정할 수 있습니다.

상품명 최대 길이를 설정하지 않으면 기본 35자로 설정됩니다.

중복단어 허용은 최대 1개까지 입니다.

""" ) self.title_length_limit_widget.leaveEvent = lambda e: self.reset_manual(self.product_name_manual_group, self.product_name_manual_label) self.title_length_limit_layout.addWidget(self.title_length_limit_label) self.title_length_limit_layout.addWidget(self.title_length_limit_input) target_layout.addWidget(self.title_length_limit_widget) # 상품명 금지어 매칭 모드 토글 self.forbidden_match_title_widget = QWidget() self.forbidden_match_title_toggle_layout = QHBoxLayout(self.forbidden_match_title_widget) self.forbidden_match_title_toggle_label = QLabel("금지어 매칭(상품명)", self) self.forbidden_match_title_toggle = ToggleSwitch(self) self.forbidden_match_title_toggle.setToolTip("금지어 매칭 방식을 선택합니다.\n포함: 금지어가 포함된 상품명을 삭제\n일치: 금지어가 완전히 일치하는 상품명을 삭제") self.forbidden_match_title_toggle.setObjectName("forbidden_match_title_toggle") self.forbidden_match_title_toggle.setOnText("포함") self.forbidden_match_title_toggle.setOffText("일치") self.forbidden_match_title_toggle.clicked.connect(lambda checked: self.universal_input_handler(self.forbidden_match_title_toggle, checked)) self.forbidden_match_title_toggle_layout.addWidget(self.forbidden_match_title_toggle_label) self.forbidden_match_title_toggle_layout.addWidget(self.forbidden_match_title_toggle) target_layout.addWidget(self.forbidden_match_title_widget) # 스크롤 영역 조립 if self.product_name_scroll: self.product_name_toggle_container.setLayout(self.product_name_toggle_container_layout) self.product_name_scroll.setWidget(self.product_name_toggle_container) self.product_name_toggle_layout.addWidget(self.product_name_scroll) # 레이아웃에 그룹 추가 (고정 비율 6:4) self.product_name_layout.addWidget(self.product_name_toggle_group, 6) self.product_name_layout.addWidget(self.product_name_manual_group, 4) # 레이아웃 추가 self.toggle_tab_widget.addTab(self.product_name_tab, "상품명") return self.product_name_tab def create_option_tab(self): """옵션 탭을 생성합니다.""" self.option_tab = QWidget() self.option_layout = QHBoxLayout(self.option_tab) # 토글 버튼 그룹 self.option_toggle_group = QGroupBox("기능") self.option_toggle_layout = QVBoxLayout(self.option_toggle_group) try: self.option_scroll = QScrollArea(self.option_toggle_group) self.option_scroll.setWidgetResizable(True) self.option_scroll.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) self.option_toggle_container = QWidget() self.option_toggle_container_layout = QVBoxLayout(self.option_toggle_container) except Exception: self.option_scroll = None self.set_group_style(self.option_toggle_group, self.option_toggle_layout, "neumorphism") self.option_toggle_layout.setSpacing(14) # 설명 그룹 self.option_manual_group = QGroupBox("매뉴얼") self.option_manual_layout = QVBoxLayout(self.option_manual_group) self.set_group_style(self.option_manual_group, self.option_manual_layout, "modern") self.option_manual_layout.setContentsMargins(1, 1, 1, 1) self.option_manual_layout.setSpacing(1) self.option_manual_label = QTextEdit(self) self.option_manual_label.setReadOnly(True) # 읽기 전용으로 설정 self.option_manual_label.setWordWrapMode(QTextOption.NoWrap) self.option_manual_label.setAcceptRichText(True) # HTML 지원 self.option_manual_label.setLineWrapMode(QTextEdit.WidgetWidth) self.option_manual_label.setWordWrapMode(QTextOption.WrapAtWordBoundaryOrAnywhere) # ↓ 스크롤바 정책: 세로는 필요할 때, 가로는 항상 숨김 self.option_manual_label.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded) self.option_manual_label.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) self.option_manual_label.setStyleSheet(""" QTextEdit { background-color: white; border: 1px solid #e0e0e0; border-radius: 4px; padding: 8px; line-height: 1.4; color: #333; } """) self.reset_manual(self.option_manual_group, self.option_manual_label) self.option_manual_layout.addWidget(self.option_manual_label) # 토글 버튼 생성 # 옵션명 번역 토글 self.optionTrans_widget = QWidget() self.optionTrans_toggle_layout = QHBoxLayout(self.optionTrans_widget) self.optionTrnas_toggle_label = QLabel("옵션명 AI 번역", self) self.optionTrnas_toggle = ToggleSwitch(self) self.optionTrnas_toggle.setObjectName("optionTrnas_toggle") self.optionTrnas_toggle.clicked.connect(lambda checked: self.universal_input_handler(self.optionTrnas_toggle, checked)) self.optionTrans_widget.enterEvent = lambda e: self.show_manual_html( self.option_manual_group, "🔤 옵션명 AI 번역", self.option_manual_label, """

소싱된 원본옵션명을 한국어로 자동 번역합니다. 구글번역과 GPT를 사용합니다.

색상, 사이즈, 타입 등의 옵션명을 정확하게 번역하여 고객이 쉽게 이해할 수 있도록 합니다.

모델명, 스펙 등 고유명사는 최대한 보존됩니다.

""" ) self.optionTrans_widget.leaveEvent = lambda e: self.reset_manual(self.option_manual_group, self.option_manual_label) self.optionTrans_toggle_layout.addWidget(self.optionTrnas_toggle_label) self.optionTrans_toggle_layout.addWidget(self.optionTrnas_toggle) o_layout = getattr(self, 'option_toggle_container_layout', self.option_toggle_layout) o_layout.addWidget(self.optionTrans_widget) # 옵션명 번역방법 토글 self.optionTrans_method_widget = QWidget() self.optionTrans_method_toggle_layout = QHBoxLayout(self.optionTrans_method_widget) self.optionTrnas_method_toggle_label = QLabel("옵션명 번역방법", self) self.optionTrnas_method_toggle = ToggleSwitch(self) self.optionTrnas_method_toggle.setOnText("GPT") self.optionTrnas_method_toggle.setOffText("기계번역") self.optionTrnas_method_toggle.setObjectName("optionTrnas_method_toggle") # self.optionTrnas_method_toggle.setEnabled(False) # self.optionTrnas_method_toggle.setChecked(True) self.optionTrnas_method_toggle.clicked.connect(lambda checked: self.universal_input_handler(self.optionTrnas_method_toggle, checked)) self.optionTrans_method_widget.enterEvent = lambda e: self.show_manual_html( self.option_manual_group, "🔤 옵션명 번역방법", self.option_manual_label, """

소싱된 원본옵션명을 한국어로 자동 번역합니다. 파파고 기반 기계번역 또는 GPT를 사용합니다.

색상, 사이즈, 타입 등의 옵션명을 정확하게 번역하여 고객이 쉽게 이해할 수 있도록 합니다.

모델명, 스펙 등 고유명사는 최대한 보존됩니다.

최대한 간결하게 정리해줍니다.

""" ) self.optionTrans_method_widget.leaveEvent = lambda e: self.reset_manual(self.option_manual_group, self.option_manual_label) self.optionTrans_method_toggle_layout.addWidget(self.optionTrnas_method_toggle_label) self.optionTrans_method_toggle_layout.addWidget(self.optionTrnas_method_toggle) o_layout.addWidget(self.optionTrans_method_widget) # 옵션명 넘버링 방식 드롭박스 self.optionNumbering_method_widget = QWidget() self.optionNumbering_method_layout = QHBoxLayout(self.optionNumbering_method_widget) self.optionNumbering_method_label = QLabel("옵션명 넘버링", self) self.optionNumbering_method_combo = QComboBox(self) # 다양한 넘버링 옵션들 추가 numbering_options = [ # 🔹 기본 ("A-Z", "alphabetic_upper"), ("a-z", "alphabetic_lower"), ("1-99", "numeric"), ("01-99", "numeric_padded_2"), # 🔹 한글 ("ㄱ-ㅎ", "korean_consonant"), ("가-하", "korean_syllable"), ("하나-아흔아홉", "korean_number_word"), # 🔹 특수 기호 조합 ("$1-$99", "dollar_numeric"), ("(1)-(99)", "parenthesis_numeric"), ("[1]-[99]", "bracket_numeric"), ("1)-99)", "right_paren_numeric"), # 🔹 로마 숫자 ("I-XX", "roman_upper"), # 대문자 로마 숫자 ("i-xx", "roman_lower"), # 소문자 로마 숫자 # 🔹 알파벳 + 숫자 조합 ("A1-Z99", "alpha_numeric_combo"), ("1A-99Z", "numeric_alpha_combo"), ("AA-ZZ", "double_alpha"), # 두 글자 알파벳 # 🔹 기타 확장 ("#1-#99", "hash_numeric"), # 해시 넘버링 ("가1-하99", "korean_alpha_numeric") ] for display_text, value in numbering_options: self.optionNumbering_method_combo.addItem(display_text, value) self.optionNumbering_method_combo.setObjectName("optionNumbering_method_combo") self.optionNumbering_method_combo.currentIndexChanged.connect( self.on_optionNumbering_method_combo_changed ) self.optionNumbering_method_widget.enterEvent = lambda e: self.show_manual_html( self.option_manual_group, "🔤 옵션명 넘버링", self.option_manual_label, """

옵션명 순서를 다양한 방식으로 넘버링합니다.

""" ) self.optionNumbering_method_widget.leaveEvent = lambda e: self.reset_manual(self.option_manual_group, self.option_manual_label) self.optionNumbering_method_layout.addWidget(self.optionNumbering_method_label) self.optionNumbering_method_layout.addWidget(self.optionNumbering_method_combo) o_layout.addWidget(self.optionNumbering_method_widget) # 옵션 이미지번역 토글 self.optionIMGTrans_widget = QWidget() self.optionIMGTrans_toggle_layout = QHBoxLayout(self.optionIMGTrans_widget) self.optionIMGTrans_toggle_label = QLabel("옵션 이미지번역", self) self.optionIMGTrans_toggle = ToggleSwitch(self) self.optionIMGTrans_toggle.setObjectName("optionIMGTrans_toggle") self.optionIMGTrans_toggle.clicked.connect self.optionIMGTrans_toggle.clicked.connect(lambda checked: self.universal_input_handler(self.optionIMGTrans_toggle, checked)) self.optionIMGTrans_widget.enterEvent = lambda e: self.show_manual_html( self.option_manual_group, "🖼️ 옵션 이미지 번역", self.option_manual_label, """

옵션 이미지에 포함된 중국어 텍스트를 한국어로 번역하여 새 이미지를 생성합니다.

옵션 이미지의 사이즈 표, 색상 설명 등을 번역하여 고객에게 제공합니다.

옵션 이미지의 특성에 따라 CPU 또는 자체 번역 방식을 선택할 수 있습니다.

""" ) self.optionIMGTrans_widget.leaveEvent = lambda e: self.reset_manual(self.option_manual_group, self.option_manual_label) self.optionIMGTrans_toggle_layout.addWidget(self.optionIMGTrans_toggle_label) self.optionIMGTrans_toggle_layout.addWidget(self.optionIMGTrans_toggle) o_layout.addWidget(self.optionIMGTrans_widget) # # ONNX 모델 타입 선택 콤보박스 # self.onnx_model_type_widget = QWidget() # self.onnx_model_type_layout = QHBoxLayout(self.onnx_model_type_widget) # self.onnx_model_type_label = QLabel("ONNX 모델 타입", self) # self.onnx_model_type_combo = QComboBox(self) # self.onnx_model_type_combo.setObjectName("onnx_model_type_combo") # self.onnx_model_type_combo.addItems(["자동 선택", "FP16 모델", "OPT 모델", "SIMP 모델"]) # self.onnx_model_type_combo.setCurrentText(self.settings_manager.get_value("option/onnx_model_type", "자동 선택")) # self.onnx_model_type_combo.currentTextChanged.connect(self.on_onnx_model_type_combo_changed) # self.onnx_model_type_widget.enterEvent = lambda e: self.show_manual_html( # self.option_manual_group, # "🧠 ONNX 모델 타입 선택", # self.option_manual_label, # """ #

GPU 번역 시 사용할 ONNX 모델의 종류를 선택합니다:

#

자동 선택: GPU 종류를 분석하여 최적의 모델 자동 선택 (권장)

#

FP16 모델: 고성능 외장 GPU용, 메모리 절약 + 고속 (RTX, RX 6000+)

#

OPT 모델: 최적화된 범용 모델, 안정성과 속도 균형 (대부분 GPU)

#

SIMP 모델: 단순화 모델, 최대 호환성 보장 (구형 GPU, 문제 발생시)

#

※ 자동 선택 시 시스템 GPU를 분석하여 최적의 모델을 선택합니다.

# """ # ) # self.onnx_model_type_widget.leaveEvent = lambda e: self.reset_manual(self.option_manual_group, self.option_manual_label) # self.onnx_model_type_layout.addWidget(self.onnx_model_type_label) # self.onnx_model_type_layout.addWidget(self.onnx_model_type_combo) # self.option_toggle_layout.addWidget(self.onnx_model_type_widget) # 첫번째 옵션 썸네일 복사 토글 self.first_option_img_to_thumb_widget = QWidget() self.first_option_img_to_thumb_toggle_layout = QHBoxLayout(self.first_option_img_to_thumb_widget) self.first_option_img_to_thumb_toggle_label = QLabel("첫 옵션 썸네일 복사", self) self.first_option_img_to_thumb_toggle = ToggleSwitch(self) self.first_option_img_to_thumb_toggle.setObjectName("first_option_img_to_thumb_toggle") self.first_option_img_to_thumb_toggle.setChecked(self.settings_manager.get_value("option/first_option_img_to_thumb", False)) self.first_option_img_to_thumb_toggle.clicked.connect(self.on_first_option_img_to_thumb_toggle_clicked) self.first_option_img_to_thumb_widget.enterEvent = lambda e: self.show_manual_html( self.option_manual_group, "📋 첫 번째 옵션 썸네일 복사", self.option_manual_label, """

첫 번째 옵션의 이미지를 상품 썸네일로 자동 복사합니다. (VIP 전용)

대표 썸네일을 첫 번째 옵션으로 설정 후 누끼작업을 합니다.

이미지 번역 시작 전에 자동으로 실행되어 썸네일을 업데이트합니다.

주의: VIP 멤버십 레벨에서만 동작합니다.

""" ) self.first_option_img_to_thumb_widget.leaveEvent = lambda e: self.reset_manual(self.option_manual_group, self.option_manual_label) self.first_option_img_to_thumb_toggle_layout.addWidget(self.first_option_img_to_thumb_toggle_label) self.first_option_img_to_thumb_toggle_layout.addWidget(self.first_option_img_to_thumb_toggle) o_layout.addWidget(self.first_option_img_to_thumb_widget) # 옵션 Auto선택 토글 self.optionAutoSelect_widget = QWidget() self.optionAutoSelect_toggle_layout = QHBoxLayout(self.optionAutoSelect_widget) self.optionAutoSelect_toggle_label = QLabel("옵션 자동선택", self) self.optionAutoSelect_toggle = ToggleSwitch(self) self.optionAutoSelect_toggle.setObjectName("optionAutoSelect_toggle") self.optionAutoSelect_toggle.clicked.connect(lambda checked: self.universal_input_handler(self.optionAutoSelect_toggle, checked)) self.optionAutoSelect_widget.enterEvent = lambda e: self.show_manual_html( self.option_manual_group, "🤖 옵션 자동 선택", self.option_manual_label, """

옵션 중 가격범위와 벗어난 미끼옵션을 자동으로 제외합니다.

원본옵션 이름과 값을 분석하여 번역 후 간소화 시킵니다.

가겨정렬 후 옵션번호를 부여합니다.

시간 절약과 동시에 옵션 관리의 일관성을 제공합니다.

""" ) self.optionAutoSelect_widget.leaveEvent = lambda e: self.reset_manual(self.option_manual_group, self.option_manual_label) self.optionAutoSelect_toggle_layout.addWidget(self.optionAutoSelect_toggle_label) self.optionAutoSelect_toggle_layout.addWidget(self.optionAutoSelect_toggle) o_layout.addWidget(self.optionAutoSelect_widget) # 최대 옵션 수 설정 self.max_option_widget = QWidget() self.max_option_layout = QHBoxLayout(self.max_option_widget) self.max_option_count_label = QLabel("최대 옵션 수:", self) self.max_option_count_input = QSpinBox() self.max_option_count_input.setObjectName("max_option_count_input") self.max_option_count_input.setMinimum(1) self.max_option_count_input.setMaximum(50) self.max_option_count_input.setValue(self.settings_manager.get_value("max_option_count", 20)) self.max_option_count_input.valueChanged.connect(lambda value: self.universal_input_handler(self.max_option_count_input, value)) self.max_option_count_input.setFixedWidth(90) self.max_option_widget.enterEvent = lambda e: self.show_manual_html( self.option_manual_group, "🔢 최대 옵션 수 설정", self.option_manual_label, """

처리할 최대 옵션의 개수를 설정합니다.

1에서 50까지의 값을 설정할 수 있습니다.

옵션이 너무 많으면 처리 시간이 길어질 수 있으므로 적절한 값을 설정하세요.

일반적으로 20개 이하의 옵션을 권장합니다.

설정한 수보다 많은 옵션이 있을 경우, 중요도에 따라 우선순위가 정해집니다.

""" ) self.max_option_widget.leaveEvent = lambda e: self.reset_manual(self.option_manual_group, self.option_manual_label) self.max_option_layout.addWidget(self.max_option_count_label) self.max_option_layout.addWidget(self.max_option_count_input) o_layout.addWidget(self.max_option_widget) # 옵션명 최대 길이 설정 self.optionName_max_length_widget = QWidget() self.optionName_max_length_layout = QHBoxLayout(self.optionName_max_length_widget) self.optionName_max_length_label = QLabel("옵션명 최대 길이:", self) self.optionName_max_length_input = QSpinBox() self.optionName_max_length_input.setObjectName("optionName_max_length_input") self.optionName_max_length_input.setMinimum(10) self.optionName_max_length_input.setMaximum(25) self.optionName_max_length_input.setValue(self.settings_manager.get_value("optionName_max_length", 25)) self.optionName_max_length_input.valueChanged.connect(lambda value: self.universal_input_handler(self.optionName_max_length_input, value)) self.optionName_max_length_input.setFixedWidth(90) self.optionName_max_length_widget.enterEvent = lambda e: self.show_manual_html( self.option_manual_group, "📏 옵션명 최대 길이 설정", self.option_manual_label, """

옵션명의 최대 길이(글자 수)를 설정합니다.

10에서 200까지의 값을 설정할 수 있습니다.

너무 긴 옵션명은 자동으로 잘려서 처리됩니다.

일반적으로 50자 이하의 옵션명을 권장합니다.

설정한 길이를 초과하는 옵션명은 자동으로 줄여집니다.

""" ) self.optionName_max_length_widget.leaveEvent = lambda e: self.reset_manual(self.option_manual_group, self.option_manual_label) self.optionName_max_length_layout.addWidget(self.optionName_max_length_label) self.optionName_max_length_layout.addWidget(self.optionName_max_length_input) o_layout.addWidget(self.optionName_max_length_widget) # 옵션 금지어 매칭 모드 토글 self.forbidden_match_option_widget = QWidget() self.forbidden_match_option_toggle_layout = QHBoxLayout(self.forbidden_match_option_widget) self.forbidden_match_option_toggle_label = QLabel("금지어 매칭(옵션)", self) self.forbidden_match_option_toggle = ToggleSwitch(self) self.forbidden_match_option_toggle.setEnabled(False) self.forbidden_match_option_toggle.setToolTip("금지어 매칭 방식을 선택합니다.\n포함: 금지어가 포함된 옵션을 삭제\n일치: 금지어가 완전히 일치하는 옵션을 삭제") self.forbidden_match_option_toggle.setObjectName("forbidden_match_option_toggle") self.forbidden_match_option_toggle.setOnText("포함") self.forbidden_match_option_toggle.setOffText("일치") self.forbidden_match_option_toggle.setChecked(False) self.forbidden_match_option_toggle.clicked.connect(lambda checked: self.universal_input_handler(self.forbidden_match_option_toggle, checked)) self.forbidden_match_option_toggle_layout.addWidget(self.forbidden_match_option_toggle_label) self.forbidden_match_option_toggle_layout.addWidget(self.forbidden_match_option_toggle) o_layout.addWidget(self.forbidden_match_option_widget) # 등록상품모드: 옵션명 셔플 토글 추가 self.option_numbering_shuffle_widget = QWidget() self.option_shuffle_layout = QHBoxLayout(self.option_numbering_shuffle_widget) self.option_numbering_shuffle_toggle_label = QLabel("옵션 넘버링 셔플", self) self.option_numbering_shuffle_toggle = ToggleSwitch(self) self.option_numbering_shuffle_toggle.setEnabled(False) # 개선때 까지 비활성화 self.option_numbering_shuffle_toggle.setChecked(False) # 개선때 까지 비활성화 self.option_numbering_shuffle_toggle.setObjectName("option_numbering_shuffle_toggle") self.option_numbering_shuffle_toggle.clicked.connect(lambda checked: self.universal_input_handler(self.option_numbering_shuffle_toggle, checked)) self.option_shuffle_layout.addWidget(self.option_numbering_shuffle_toggle_label) self.option_shuffle_layout.addWidget(self.option_numbering_shuffle_toggle) o_layout.addWidget(self.option_numbering_shuffle_widget) # 스크롤 영역 조립 if self.option_scroll: self.option_toggle_container.setLayout(self.option_toggle_container_layout) self.option_scroll.setWidget(self.option_toggle_container) self.option_toggle_layout.addWidget(self.option_scroll) # 레이아웃에 그룹 추가 (고정 비율 6:4) self.option_layout.addWidget(self.option_toggle_group, 6) self.option_layout.addWidget(self.option_manual_group, 4) # 레이아웃 추가 self.toggle_tab_widget.addTab(self.option_tab, "옵션") return self.option_tab def create_tag_tab(self): """태그 탭을 생성합니다.""" self.tag_tab = QWidget() self.tag_layout = QHBoxLayout(self.tag_tab) # 토글 버튼 그룹 self.tags_toggle_group = QGroupBox("기능") self.tags_toggle_layout = QVBoxLayout(self.tags_toggle_group) try: self.tag_scroll = QScrollArea(self.tags_toggle_group) self.tag_scroll.setWidgetResizable(True) self.tag_scroll.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) self.tags_toggle_container = QWidget() self.tags_toggle_container_layout = QVBoxLayout(self.tags_toggle_container) except Exception: self.tag_scroll = None self.set_group_style(self.tags_toggle_group, self.tags_toggle_layout, "neumorphism") self.tags_toggle_layout.setSpacing(10) # 설명 그룹 self.tag_manual_group = QGroupBox("매뉴얼") self.tag_manual_layout = QVBoxLayout(self.tag_manual_group) self.set_group_style(self.tag_manual_group, self.tag_manual_layout, "modern") self.tag_manual_layout.setContentsMargins(1, 1, 1, 1) self.tag_manual_layout.setSpacing(1) self.tag_manual_label = QTextEdit(self) self.tag_manual_label.setReadOnly(True) # 읽기 전용으로 설정 self.tag_manual_label.setWordWrapMode(QTextOption.NoWrap) self.tag_manual_label.setAcceptRichText(True) # HTML 지원 self.tag_manual_label.setLineWrapMode(QTextEdit.WidgetWidth) self.tag_manual_label.setWordWrapMode(QTextOption.WrapAtWordBoundaryOrAnywhere) # ↓ 스크롤바 정책: 세로는 필요할 때, 가로는 항상 숨김 self.tag_manual_label.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded) self.tag_manual_label.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) self.tag_manual_label.setStyleSheet(""" QTextEdit { background-color: white; border: 1px solid #e0e0e0; border-radius: 4px; padding: 8px; line-height: 1.4; color: #333; } """) self.reset_manual(self.tag_manual_group, self.tag_manual_label) self.tag_manual_layout.addWidget(self.tag_manual_label) # 토글 버튼 생성 # 태그 수정 토글 self.tag_widget = QWidget() self.tag_toggle_layout = QHBoxLayout(self.tag_widget) self.tag_toggle_label = QLabel("태그 수정", self) self.tag_toggle = ToggleSwitch(self) self.tag_toggle.setObjectName("tag_toggle") self.tag_toggle.clicked.connect(lambda checked: self.universal_input_handler(self.tag_toggle, checked)) self.tag_widget.enterEvent = lambda e: self.show_manual_html( self.tag_manual_group, "🏷️ 태그 수정", self.tag_manual_label, """

태그 입력 기능을 활성화/비활성화합니다.

기본적으로 중복태그 제거, 경고키워드 제거 기능이 포함됩니다.

또한 금지어 설정이 되어있을 경우, 금지어 필터링이 적용됩니다.

기능 OFF 시 상품 수집때의 태그를 유지합니다.

""" ) self.tag_widget.leaveEvent = lambda e: self.reset_manual(self.tag_manual_group, self.tag_manual_label) self.tag_toggle_layout.addWidget(self.tag_toggle_label) self.tag_toggle_layout.addWidget(self.tag_toggle) # 레이아웃에 위젯 추가 t_layout = getattr(self, 'tags_toggle_container_layout', self.tags_toggle_layout) t_layout.addWidget(self.tag_widget) # 태그 생성 방식 선택 콤보박스 self.tag_method_widget = QWidget() self.tag_method_layout = QHBoxLayout(self.tag_method_widget) self.tag_method_label = QLabel("태그 생성 방식", self) self.tag_method_combo = QComboBox(self) self.tag_method_combo.setObjectName("tag_method_combo") self.tag_method_combo.addItem("상품명기반생성", "product_name") self.tag_method_combo.addItem("AI태그생성", "ai") # VIP 전용: 렌즈기반 태그 옵션 if self.is_vip_user(): self.tag_method_combo.addItem("렌즈기반태그 (VIP)", "lens") self.tag_method_combo.setCurrentIndex(0) # 기본값: 상품명기반생성 self.tag_method_combo.currentIndexChanged.connect(lambda: self.on_tag_method_changed()) # 도움말 텍스트 생성 (VIP 여부에 따라 다르게) tag_method_help_text = """

상품명기반생성 (기본): 상품명에서 앞 4단어를 추출하여 키워드 검색 후 태그 추가

AI태그생성: GPT를 이용한 네이버 스마트스토어 SEO 기준 태그 생성

""" if self.is_vip_user(): tag_method_help_text = """

상품명기반생성 (기본): 상품명에서 앞 4단어를 추출하여 키워드 검색 후 태그 추가

AI태그생성: GPT를 이용한 네이버 스마트스토어 SEO 기준 태그 생성

렌즈기반태그 (VIP): 쇼핑렌즈로 검색한 실제 판매상품의 태그를 직접 가져옵니다

""" self.tag_method_widget.enterEvent = lambda e: self.show_manual_html( self.tag_manual_group, "🏷️ 태그 생성 방식", self.tag_manual_label, tag_method_help_text ) self.tag_method_widget.leaveEvent = lambda e: self.reset_manual(self.tag_manual_group, self.tag_manual_label) self.tag_method_layout.addWidget(self.tag_method_label) self.tag_method_layout.addWidget(self.tag_method_combo) t_layout.addWidget(self.tag_method_widget) # 모든 태그 삭제 토글 self.delete_all_tags_widget = QWidget() self.delete_all_tags_toggle_layout = QHBoxLayout(self.delete_all_tags_widget) self.delete_all_tags_toggle_label = QLabel("모든 태그 삭제", self) self.delete_all_tags_toggle = ToggleSwitch(self) self.delete_all_tags_toggle.setOnText("삭제") self.delete_all_tags_toggle.setOffText("유지") self.delete_all_tags_toggle.setObjectName("delete_all_tags_toggle") self.delete_all_tags_toggle.clicked.connect(lambda checked: self.universal_input_handler(self.delete_all_tags_toggle, checked)) self.delete_all_tags_widget.enterEvent = lambda e: self.show_manual_html( self.tag_manual_group, "🏷️ 모든 태그 삭제", self.tag_manual_label, """

새로운 태그 입력 전 모든 태그를 삭제합니다.

기능 ON 시 모든 태그를 삭제합니다.

기능 OFF 시 기존 태그를 유지합니다.

""" ) self.delete_all_tags_widget.leaveEvent = lambda e: self.reset_manual(self.tag_manual_group, self.tag_manual_label) self.delete_all_tags_toggle_layout.addWidget(self.delete_all_tags_toggle_label) self.delete_all_tags_toggle_layout.addWidget(self.delete_all_tags_toggle) # 레이아웃에 위젯 추가 t_layout.addWidget(self.delete_all_tags_widget) # 태그 금지어 매칭 모드 토글 self.forbidden_match_tag_widget = QWidget() self.forbidden_match_tag_toggle_layout = QHBoxLayout(self.forbidden_match_tag_widget) self.forbidden_match_tag_toggle_label = QLabel("금지어 매칭(태그)", self) self.forbidden_match_tag_toggle = ToggleSwitch(self) self.forbidden_match_tag_toggle.setToolTip("금지어 매칭 방식을 선택합니다.\n포함: 금지어가 포함된 태그를 삭제\n일치: 금지어가 완전히 일치하는 태그를 삭제") self.forbidden_match_tag_toggle.setObjectName("forbidden_match_tag_toggle") self.forbidden_match_tag_toggle.setOnText("포함") self.forbidden_match_tag_toggle.setOffText("일치") self.forbidden_match_tag_toggle.clicked.connect(lambda checked: self.universal_input_handler(self.forbidden_match_tag_toggle, checked)) self.forbidden_match_tag_toggle_layout.addWidget(self.forbidden_match_tag_toggle_label) self.forbidden_match_tag_toggle_layout.addWidget(self.forbidden_match_tag_toggle) t_layout.addWidget(self.forbidden_match_tag_widget) # 스크롤 영역 조립 if self.tag_scroll: self.tags_toggle_container.setLayout(self.tags_toggle_container_layout) self.tag_scroll.setWidget(self.tags_toggle_container) self.tags_toggle_layout.addWidget(self.tag_scroll) # 레이아웃에 그룹 추가 (고정 비율 6:4) self.tag_layout.addWidget(self.tags_toggle_group, 6) self.tag_layout.addWidget(self.tag_manual_group, 4) # 레이아웃 추가 self.toggle_tab_widget.addTab(self.tag_tab, "태그") return self.tag_tab def create_price_tab(self): """가격 탭을 생성합니다.""" self.price_tab = QWidget() self.price_layout = QHBoxLayout(self.price_tab) # 토글 버튼 그룹 self.prices_toggle_group = QGroupBox("기능") self.prices_toggle_layout = QVBoxLayout(self.prices_toggle_group) self.set_group_style(self.prices_toggle_group, self.prices_toggle_layout, "neumorphism") self.prices_toggle_layout.setSpacing(10) # 설명 그룹 self.price_manual_group = QGroupBox("매뉴얼") self.price_manual_layout = QVBoxLayout(self.price_manual_group) self.set_group_style(self.price_manual_group, self.price_manual_layout, "modern") self.price_manual_layout.setContentsMargins(1, 1, 1, 1) self.price_manual_layout.setSpacing(1) self.price_manual_label = QTextEdit(self) self.price_manual_label.setReadOnly(True) # 읽기 전용으로 설정 self.price_manual_label.setWordWrapMode(QTextOption.NoWrap) self.price_manual_label.setAcceptRichText(True) # HTML 지원 self.price_manual_label.setLineWrapMode(QTextEdit.WidgetWidth) self.price_manual_label.setWordWrapMode(QTextOption.WrapAtWordBoundaryOrAnywhere) # ↓ 스크롤바 정책: 세로는 필요할 때, 가로는 항상 숨김 self.price_manual_label.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded) self.price_manual_label.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) self.price_manual_label.setStyleSheet(""" QTextEdit { background-color: white; border: 1px solid #e0e0e0; border-radius: 4px; padding: 8px; line-height: 1.4; color: #333; } """) self.reset_manual(self.price_manual_group, self.price_manual_label) self.price_manual_layout.addWidget(self.price_manual_label) # 토글 버튼 생성 # 가격 수정 토글 self.price_widget = QWidget() self.price_toggle_layout = QHBoxLayout(self.price_widget) self.price_toggle_label = QLabel("적정 가격 자동 수정", self) self.price_toggle = ToggleSwitch(self) self.price_toggle.setObjectName("price_toggle") self.price_toggle.clicked.connect(lambda checked: self.universal_input_handler(self.price_toggle, checked)) self.price_widget.enterEvent = lambda e: self.show_manual_html( self.price_manual_group, "💰 적정 가격 자동 수정", self.price_manual_label, """

가격설정에 따라 자동으로 가격을 수정합니다.

-상품정보 수집결과에 따라 수집된 판매된 가격(50%)과

계산된가격(20%), 원가배율(30%)의 비율로

적정 판매가격을 산출하여 적용합니다.

-계산된 가격의 실제 마진율은 약 24%전후로 가격설정에서 조정가능합니다.

-가격설정 버튼을 통해 더 세부적인 가격 정책을 설정할 수 있습니다.

-가격설정 버튼에는 더하기마진을 배송비에 녹이는 기능 버튼이 포함되어 있습니다.

-가격설정에서는 크무비 상품의 카테고리를 지정하여

카테고리 설정에서 크무비카테고리의 추가배송비 설정

금지 카테고리 설정등을 할 수 있습니다.

""" ) self.price_widget.leaveEvent = lambda e: self.reset_manual(self.price_manual_group, self.price_manual_label) self.price_toggle_layout.addWidget(self.price_toggle_label) self.price_toggle_layout.addWidget(self.price_toggle) self.prices_toggle_layout.addWidget(self.price_widget) # 가격초과제외 토글 self.remove_overprice_widget = QWidget() self.remove_overprice_toggle_layout = QHBoxLayout(self.remove_overprice_widget) self.remove_overprice_toggle_label = QLabel("가격초과제외", self) self.remove_overprice_toggle = ToggleSwitch(self) self.remove_overprice_toggle.setObjectName("remove_overprice_toggle") self.remove_overprice_toggle.clicked.connect(lambda checked: self.universal_input_handler(self.remove_overprice_toggle, checked)) self.remove_overprice_widget.enterEvent = lambda e: self.show_manual_html( self.price_manual_group, "⚠️ 가격초과제외", self.price_manual_label, """

선택된 옵션들의 가격이 각 마켓의 대표가격 범위를 초과할 경우

해당하는 모든 옵션을 제외하는 버튼을 클릭하게 합니다.

대표가격을 정하는 최저옵션가 기준과 최대업로드갯수 기준에 따라 범위는 달라지며

11번가를 제외한 나머지 마켓은 대표가격 대비 -50% ~ +50% 범위로 제한합니다.

""" ) self.remove_overprice_widget.leaveEvent = lambda e: self.reset_manual(self.price_manual_group, self.price_manual_label) self.remove_overprice_toggle_layout.addWidget(self.remove_overprice_toggle_label) self.remove_overprice_toggle_layout.addWidget(self.remove_overprice_toggle) self.prices_toggle_layout.addWidget(self.remove_overprice_widget) # 가격설정 버튼 self.cmb_button_widget = QWidget() self.cmb_button_layout = QHBoxLayout(self.cmb_button_widget) self.cmb_button = QPushButton('가격설정', self) self.cmb_button.setObjectName("cmb_button") self.cmb_button.setToolTip('상품가격 설정 및 크무비 단계 및 가격설정, 그리고 금지카테고리 설정을 할수 있습니다.') self.cmb_button.setFixedWidth(100) self.set_PUSHBTN_style(self.cmb_button, "modern") self.cmb_button.clicked.connect(self.on_cmb_button_clicked) self.cmb_button_widget.enterEvent = lambda e: self.show_manual_html( self.price_manual_group, "⚙️ 가격설정", self.price_manual_label, """

상품가설정, 크무비 추가배송비 설정, 금지카테고리 설정 메뉴를 엽니다.

여기서 다음과 같은 설정이 가능합니다:

상품 특성에 따른 맞춤형 가격 전략을 구성할 수 있습니다.

""" ) self.cmb_button_widget.leaveEvent = lambda e: self.reset_manual(self.price_manual_group, self.price_manual_label) self.cmb_button_layout.addWidget(self.cmb_button) self.prices_toggle_layout.addWidget(self.cmb_button_widget) # 등록상품모드: 가격범위변경 토글 + 스핀박스 (기본 숨김) self.price_range_widget = QWidget() self.price_range_layout = QHBoxLayout(self.price_range_widget) self.price_range_toggle_label = QLabel("가격범위변경", self) self.price_range_toggle = ToggleSwitch(self) self.price_range_toggle.setObjectName("price_range_toggle") self.price_range_toggle.clicked.connect(lambda checked: self.universal_input_handler(self.price_range_toggle, checked)) self.price_range_spin_label = QLabel("변경범위(±%)", self) self.price_range_spin = QSpinBox(self) self.price_range_spin.setObjectName("price_range_spin") self.price_range_spin.setMinimum(-3) self.price_range_spin.setMaximum(3) self.price_range_spin.setSuffix("%") self.price_range_spin.setValue(int(self.settings_manager.get_value("price/range_percent", 0))) self.price_range_spin.valueChanged.connect(lambda value: self.universal_input_handler(self.price_range_spin, value)) # 기본은 숨김, 토글 ON 시 보이도록 위젯맵에서 제어 self.price_range_spin_label.setVisible(False) self.price_range_spin.setVisible(False) self.price_range_layout.addWidget(self.price_range_toggle_label) self.price_range_layout.addWidget(self.price_range_toggle) self.price_range_layout.addWidget(self.price_range_spin_label) self.price_range_layout.addWidget(self.price_range_spin) self.prices_toggle_layout.addWidget(self.price_range_widget) # 레이아웃에 그룹 추가 self.price_layout.addWidget(self.prices_toggle_group, 3) self.price_layout.addWidget(self.price_manual_group, 7) # 레이아웃 추가 self.toggle_tab_widget.addTab(self.price_tab, "가격") return self.price_tab def create_thumbnail_tab(self): """썸네일 탭을 생성합니다.""" self.thumbnail_tab = QWidget() self.thumbnail_layout = QHBoxLayout(self.thumbnail_tab) # 토글 버튼 그룹 self.thumbnail_toggle_group = QGroupBox("기능") self.thumbnail_toggle_layout = QVBoxLayout(self.thumbnail_toggle_group) self.set_group_style(self.thumbnail_toggle_group, self.thumbnail_toggle_layout, "neumorphism") self.thumbnail_toggle_layout.setSpacing(10) # 설명 그룹 self.thumbnail_manual_group = QGroupBox("매뉴얼") self.thumbnail_manual_layout = QVBoxLayout(self.thumbnail_manual_group) self.set_group_style(self.thumbnail_manual_group, self.thumbnail_manual_layout, "modern") self.thumbnail_manual_layout.setContentsMargins(1, 1, 1, 1) self.thumbnail_manual_layout.setSpacing(1) self.thumbnail_manual_label = QTextEdit(self) self.thumbnail_manual_label.setReadOnly(True) # 읽기 전용으로 설정 self.thumbnail_manual_label.setWordWrapMode(QTextOption.NoWrap) self.thumbnail_manual_label.setAcceptRichText(True) # HTML 지원 self.thumbnail_manual_label.setLineWrapMode(QTextEdit.WidgetWidth) self.thumbnail_manual_label.setWordWrapMode(QTextOption.WrapAtWordBoundaryOrAnywhere) # ↓ 스크롤바 정책: 세로는 필요할 때, 가로는 항상 숨김 self.thumbnail_manual_label.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded) self.thumbnail_manual_label.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) self.thumbnail_manual_label.setStyleSheet(""" QTextEdit { background-color: white; border: 1px solid #e0e0e0; border-radius: 4px; padding: 8px; line-height: 1.4; color: #333; } """) self.reset_manual(self.thumbnail_manual_group, self.thumbnail_manual_label) self.thumbnail_manual_layout.addWidget(self.thumbnail_manual_label) # 토글 버튼 생성 # 썸네일 번역 토글 self.thumb_widget = QWidget() self.thumb_toggle_layout = QHBoxLayout(self.thumb_widget) self.thumb_toggle_label = QLabel("썸네일 번역", self) self.thumb_toggle = ToggleSwitch(self) self.thumb_toggle.setObjectName("thumb_toggle") self.thumb_toggle.clicked.connect(lambda checked: self.universal_input_handler(self.thumb_toggle, checked)) self.thumb_widget.enterEvent = lambda e: self.show_manual_html( self.thumbnail_manual_group, "🖼️ 썸네일 번역", self.thumbnail_manual_label, """

상품의 썸네일에 포함된 중국어 텍스트를 한국어로 번역합니다.

번역된 텍스트는 원본 이미지의 위치와 스타일을 최대한 유지합니다.

번역 엔진(CPU/자체번역)을 선택가능

""" ) self.thumb_widget.leaveEvent = lambda e: self.reset_manual(self.thumbnail_manual_group, self.thumbnail_manual_label) self.thumb_toggle_layout.addWidget(self.thumb_toggle_label) self.thumb_toggle_layout.addWidget(self.thumb_toggle) self.thumbnail_toggle_layout.addWidget(self.thumb_widget) # 썸네일 누끼 토글 self.thumb_nukki_widget = QWidget() self.thumb_nukki_toggle_layout = QHBoxLayout(self.thumb_nukki_widget) self.thumb_nukki_toggle_label = QLabel("썸네일누끼", self) self.thumb_nukki_toggle = ToggleSwitch(self) self.thumb_nukki_toggle.setObjectName("thumb_nukki_toggle") self.thumb_nukki_toggle.clicked.connect(lambda checked: self.universal_input_handler(self.thumb_nukki_toggle, checked)) self.thumb_nukki_widget.enterEvent = lambda e: self.show_manual_html( self.thumbnail_manual_group, "✂️ 썸네일 누끼", self.thumbnail_manual_label, """

상품 썸네일에서 배경을 자동으로 제거하여 깔끔한 상품이미지를 생성합니다.

제품 이미지를 더 선명하게 부각시키고 통일된 스타일을 제공합니다.

누끼 갯수 설정을 통해 처리할 이미지 수를 지정할 수 있습니다.

기본설정으로 첫 번째 이미지만 누끼 처리가 적용됩니다.

""" ) self.thumb_nukki_widget.leaveEvent = lambda e: self.reset_manual(self.thumbnail_manual_group, self.thumbnail_manual_label) self.thumb_nukki_toggle_layout.addWidget(self.thumb_nukki_toggle_label) self.thumb_nukki_toggle_layout.addWidget(self.thumb_nukki_toggle) self.thumbnail_toggle_layout.addWidget(self.thumb_nukki_widget) # 누끼 갯수 설정 self.thumb_rmb_widget = QWidget() self.thumb_rmb_layout = QHBoxLayout(self.thumb_rmb_widget) self.thumb_rmb_count_label = QLabel("누끼 갯수:", self) self.thumb_rmb_count_input = QSpinBox() self.thumb_rmb_count_input.setObjectName("thumb_rmb_count_input") self.thumb_rmb_count_input.setMinimum(1) self.thumb_rmb_count_input.setMaximum(10) self.thumb_rmb_count_input.setValue(self.settings_manager.get_value("thumb_rmb_count", 1)) self.thumb_rmb_count_input.valueChanged.connect(lambda value: self.update_thumb_rmb_count(value)) self.thumb_rmb_count_input.valueChanged.connect(lambda value: self.universal_input_handler(self.thumb_rmb_count_input, value)) self.thumb_rmb_count_input.setFixedWidth(60) self.thumb_rmb_widget.enterEvent = lambda e: self.show_manual_html( self.thumbnail_manual_group, "🔢 누끼 갯수 설정", self.thumbnail_manual_label, """

배경을 제거할 썸네일 이미지의 개수를 설정합니다.

1에서 10까지의 값을 설정할 수 있습니다.

일반적으로 첫 번째 이미지부터 설정한 개수만큼 누끼 처리가 적용됩니다.

""" ) self.thumb_rmb_widget.leaveEvent = lambda e: self.reset_manual(self.thumbnail_manual_group, self.thumbnail_manual_label) self.thumb_rmb_layout.addWidget(self.thumb_rmb_count_label) self.thumb_rmb_layout.addWidget(self.thumb_rmb_count_input) self.thumbnail_toggle_layout.addWidget(self.thumb_rmb_widget) # 누끼 서버 self.nukki_server_widget = QWidget() self.nukki_server_toggle_layout = QHBoxLayout(self.nukki_server_widget) self.nukki_server_toggle_label = QLabel("self누끼", self) self.nukki_server_toggle = ToggleSwitch(self) self.nukki_server_toggle.setObjectName("nukki_server_toggle") self.nukki_server_toggle.clicked.connect(lambda checked: self.universal_input_handler(self.nukki_server_toggle, checked)) self.nukki_server_widget.enterEvent = lambda e: self.show_manual_html( self.thumbnail_manual_group, "✂️ self누끼", self.thumbnail_manual_label, """

누끼 서버가 죽었을때 백업동작 설정을 사용할지 여부를 결정합니다.

체크시 누끼서버가 죽었을때 자동으로 자체 배경제거를 수행합니다. 속도가 느립니다.

""" ) self.nukki_server_widget.leaveEvent = lambda e: self.reset_manual(self.thumbnail_manual_group, self.thumbnail_manual_label) self.nukki_server_toggle_layout.addWidget(self.nukki_server_toggle_label) self.nukki_server_toggle_layout.addWidget(self.nukki_server_toggle) self.thumbnail_toggle_layout.addWidget(self.nukki_server_widget) # 등록상품모드: 대표썸네일변경 토글 (기본 OFF) self.thumb_represent_widget = QWidget() self.thumb_represent_layout = QHBoxLayout(self.thumb_represent_widget) self.thumb_represent_toggle_label = QLabel("대표썸네일변경", self) self.thumb_represent_toggle = ToggleSwitch(self) self.thumb_represent_toggle.setObjectName("thumb_represent_toggle") self.thumb_represent_toggle.clicked.connect(lambda checked: self.universal_input_handler(self.thumb_represent_toggle, checked)) self.thumb_represent_layout.addWidget(self.thumb_represent_toggle_label) self.thumb_represent_layout.addWidget(self.thumb_represent_toggle) self.thumbnail_toggle_layout.addWidget(self.thumb_represent_widget) # 레이아웃에 그룹 추가 self.thumbnail_layout.addWidget(self.thumbnail_toggle_group, 3) self.thumbnail_layout.addWidget(self.thumbnail_manual_group, 7) # 레이아웃 추가 self.toggle_tab_widget.addTab(self.thumbnail_tab, "썸네일") return self.thumbnail_tab def create_detail_tab(self): """상세페이지 탭을 생성합니다.""" self.detail_tab = QWidget() self.detail_layout = QHBoxLayout(self.detail_tab) # 토글 버튼 그룹 self.detail_toggle_group = QGroupBox("기능") self.detail_toggle_layout = QVBoxLayout(self.detail_toggle_group) self.set_group_style(self.detail_toggle_group, self.detail_toggle_layout, "neumorphism") self.detail_toggle_layout.setSpacing(10) # 설명 그룹 self.detail_manual_group = QGroupBox("매뉴얼") self.detail_manual_layout = QVBoxLayout(self.detail_manual_group) self.set_group_style(self.detail_manual_group, self.detail_manual_layout, "modern") self.detail_manual_layout.setContentsMargins(1, 1, 1, 1) self.detail_manual_layout.setSpacing(1) self.detail_manual_label = QTextEdit(self) self.detail_manual_label.setReadOnly(True) # 읽기 전용으로 설정 self.detail_manual_label.setWordWrapMode(QTextOption.NoWrap) self.detail_manual_label.setAcceptRichText(True) # HTML 지원 self.detail_manual_label.setLineWrapMode(QTextEdit.WidgetWidth) self.detail_manual_label.setWordWrapMode(QTextOption.WrapAtWordBoundaryOrAnywhere) # ↓ 스크롤바 정책: 세로는 필요할 때, 가로는 항상 숨김 self.detail_manual_label.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded) self.detail_manual_label.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) self.detail_manual_label.setStyleSheet(""" QTextEdit { background-color: white; border: 1px solid #e0e0e0; border-radius: 4px; padding: 8px; line-height: 1.4; color: #333; } """) self.reset_manual(self.detail_manual_group, self.detail_manual_label) self.detail_manual_layout.addWidget(self.detail_manual_label) # 토글 버튼 생성 # 상페 옵션명 추가 토글 self.detail_Option_widget = QWidget() self.detail_Option_toggle_layout = QHBoxLayout(self.detail_Option_widget) self.detail_Option_toggle_label = QLabel("상페설명 & 옵션명추가", self) self.detail_Option_toggle = ToggleSwitch(self) self.detail_Option_toggle.setObjectName("detail_Option_toggle") self.detail_Option_toggle.clicked.connect(lambda checked: self.universal_input_handler(self.detail_Option_toggle, checked)) self.detail_Option_widget.enterEvent = lambda e: self.show_manual_html( self.detail_manual_group, "📝 상세페이지 설명 & 옵션명 추가", self.detail_manual_label, """

상세페이지 상단에 마켓 설명과 옵션명을 자동으로 추가합니다.

한국어로 번역된 상품 정보를 고객이 쉽게 확인할 수 있도록 합니다.

마켓의 가격범위 정책으로 인해 제외된 상품의 옵션도 함께 입력할 수 있습니다.

옵션 정보와 함께 제공되는 상세 설명으로 구매 결정을 돕습니다.

""" ) self.detail_Option_widget.leaveEvent = lambda e: self.reset_manual(self.detail_manual_group, self.detail_manual_label) self.detail_Option_toggle_layout.addWidget(self.detail_Option_toggle_label) self.detail_Option_toggle_layout.addWidget(self.detail_Option_toggle) self.detail_toggle_layout.addWidget(self.detail_Option_widget) # 상페텍스트 버튼 self.detail_text_button_widget = QWidget() self.detail_text_button_layout = QHBoxLayout(self.detail_text_button_widget) self.detail_text_button = QPushButton('상페텍스트', self) self.detail_text_button.setObjectName("detail_text_button") self.detail_text_button.setFixedWidth(100) self.set_PUSHBTN_style(self.detail_text_button, "modern") self.detail_text_button.clicked.connect(self.on_detail_text_button_clicked) # self.detail_text_button.setEnabled(False) # 초기 상태는 비활성화 self.detail_text_button_widget.enterEvent = lambda e: self.show_manual_html( self.detail_manual_group, "📄 상세페이지 텍스트 설정", self.detail_manual_label, """

상세페이지에 삽입할 텍스트 템플릿과 스타일을 설정합니다.

상품 정보, 배송 안내, 교환/반품 정책 등의 템플릿을 관리할 수 있습니다.

마크다운, HTML 형식을 지원하여 다양한 스타일 적용이 가능합니다.

왼쪽은 편집, 오른쪽은 미리보기

""" ) self.detail_text_button_widget.leaveEvent = lambda e: self.reset_manual(self.detail_manual_group, self.detail_manual_label) self.detail_text_button_layout.addWidget(self.detail_text_button) self.detail_toggle_layout.addWidget(self.detail_text_button_widget) # # VIP용 등록모드 상세페이지 수정 토글 # self.vip_detail_edit_widget = QWidget() # self.vip_detail_edit_toggle_layout = QHBoxLayout(self.vip_detail_edit_widget) # self.vip_detail_edit_toggle_label = QLabel("상페수정(등록모드)", self) # self.vip_detail_edit_toggle = ToggleSwitch(self) # self.vip_detail_edit_toggle.setEnabled(False) # 개선때 까지 비활성화 # self.vip_detail_edit_toggle.setChecked(False) # 개선때 까지 비활성화 # self.vip_detail_edit_toggle.setObjectName("vip_detail_edit_toggle") # self.vip_detail_edit_toggle.clicked.connect(lambda checked: self.universal_input_handler(self.vip_detail_edit_toggle, checked)) # self.vip_detail_edit_widget.enterEvent = lambda e: self.show_manual_html( # self.detail_manual_group, # "상세페이지 수정 (등록모드 전용)", # self.detail_manual_label, # """ #

등록모드 상세페이지 수정

#

등록모드에서 상세페이지를 수정할 수 있습니다.

#

활성화 시 선택마켓에서 지정된 스마트스토어의 사업자 이름 + 랜덤 홍보문구가 첫 줄에 자동 추가됩니다.

# """ # ) # self.vip_detail_edit_widget.leaveEvent = lambda e: self.reset_manual(self.detail_manual_group, self.detail_manual_label) # self.vip_detail_edit_toggle_layout.addWidget(self.vip_detail_edit_toggle_label) # self.vip_detail_edit_toggle_layout.addWidget(self.vip_detail_edit_toggle) # self.detail_toggle_layout.addWidget(self.vip_detail_edit_widget) # 상페 이미지 번역 토글 self.detail_IMGTrans_widget = QWidget() self.detail_IMGTrans_toggle_layout = QHBoxLayout(self.detail_IMGTrans_widget) self.detail_IMGTrans_toggle_label = QLabel("상페 이미지 번역", self) self.detail_IMGTrans_toggle = ToggleSwitch(self) self.detail_IMGTrans_toggle.setObjectName("detail_IMGTrans_toggle") self.detail_IMGTrans_toggle.clicked.connect(lambda checked: self.universal_input_handler(self.detail_IMGTrans_toggle, checked)) self.detail_IMGTrans_widget.enterEvent = lambda e: self.show_manual_html( self.detail_manual_group, "🖼️ 상세페이지 이미지 번역", self.detail_manual_label, """

상세페이지 이미지에 포함된 중국어 텍스트를 한국어로 번역합니다.

번역 엔진을 선택하여 이미지 특성에 맞는 번역 방식을 적용할 수 있습니다.

워터마크 기능을 함께 사용하면 번역된 이미지에 브랜드 마크를 추가할 수 있습니다.

""" ) self.detail_IMGTrans_widget.leaveEvent = lambda e: self.reset_manual(self.detail_manual_group, self.detail_manual_label) self.detail_IMGTrans_toggle_layout.addWidget(self.detail_IMGTrans_toggle_label) self.detail_IMGTrans_toggle_layout.addWidget(self.detail_IMGTrans_toggle) self.detail_toggle_layout.addWidget(self.detail_IMGTrans_widget) # 동시 번역 수 스핀박스 (별도 라인으로 분리) self.detail_concurrency_widget = QWidget() self.detail_concurrency_layout = QHBoxLayout(self.detail_concurrency_widget) self.detail_concurrency_layout.setContentsMargins(20, 0, 0, 0) # 들여쓰기 효과 self.detail_concurrency_label = QLabel("동시 번역 수:", self) self.detail_concurrency_label.setToolTip("상세페이지 이미지 동시 번역 수") self.detail_concurrency_spinbox = QSpinBox(self) self.detail_concurrency_spinbox.setRange(1, 4) self.detail_concurrency_spinbox.setToolTip("상세페이지 이미지 동시 번역 수") self.detail_concurrency_spinbox.setObjectName("detail_concurrency_spinbox") # 설정 로드 saved_limit = self.settings_manager.get_value("detail/detail_concurrency_limit", 2) self.detail_concurrency_spinbox.setValue(int(saved_limit)) # 값 변경 시 저장 self.detail_concurrency_spinbox.valueChanged.connect(lambda val: self.universal_input_handler(self.detail_concurrency_spinbox, val)) self.detail_concurrency_layout.addWidget(self.detail_concurrency_label) self.detail_concurrency_layout.addWidget(self.detail_concurrency_spinbox) self.detail_concurrency_layout.addStretch() self.detail_toggle_layout.addWidget(self.detail_concurrency_widget) # 상페 홍보문구 활성화 토글 self.detail_promo_enabled_widget = QWidget() self.detail_promo_enabled_toggle_layout = QHBoxLayout(self.detail_promo_enabled_widget) self.detail_promo_enabled_toggle_label = QLabel("상페 홍보문구", self) self.detail_promo_enabled_toggle = ToggleSwitch(self) self.detail_promo_enabled_toggle.setObjectName("detail_promo_enabled_toggle") self.detail_promo_enabled_toggle.clicked.connect(lambda checked: self.universal_input_handler(self.detail_promo_enabled_toggle, checked)) self.detail_promo_enabled_widget.enterEvent = lambda e: self.show_manual_html( self.detail_manual_group, "📢 상세페이지 홍보문구", self.detail_manual_label, """

등록모드에서 상세페이지 상단 또는 하단에 홍보문구를 자동으로 추가합니다.

사업자 이름과 랜덤 홍보문구가 결합되어 표시됩니다.

홍보문구 위치를 상단 또는 하단으로 선택할 수 있습니다.

""" ) self.detail_promo_enabled_widget.leaveEvent = lambda e: self.reset_manual(self.detail_manual_group, self.detail_manual_label) self.detail_promo_enabled_toggle_layout.addWidget(self.detail_promo_enabled_toggle_label) self.detail_promo_enabled_toggle_layout.addWidget(self.detail_promo_enabled_toggle) self.detail_toggle_layout.addWidget(self.detail_promo_enabled_widget) # 홍보문구 위치 설정 (별도 라인으로 분리) self.detail_promo_position_widget = QWidget() self.detail_promo_position_layout = QHBoxLayout(self.detail_promo_position_widget) self.detail_promo_position_layout.setContentsMargins(20, 0, 0, 0) # 들여쓰기 효과 self.detail_promo_position_label = QLabel("홍보문구 위치:", self) self.detail_promo_position_label.setToolTip("상세페이지 홍보문구 위치") self.detail_promo_position_combo = QComboBox(self) self.detail_promo_position_combo.addItems(["top", "bottom"]) self.detail_promo_position_combo.setToolTip("상세페이지 홍보문구 위치") self.detail_promo_position_combo.setObjectName("detail_promo_position_combo") # 값 변경 시 저장 self.detail_promo_position_combo.currentTextChanged.connect(lambda val: self.universal_input_handler(self.detail_promo_position_combo, val)) self.detail_promo_position_layout.addWidget(self.detail_promo_position_label) self.detail_promo_position_layout.addWidget(self.detail_promo_position_combo) self.detail_promo_position_layout.addStretch() self.detail_toggle_layout.addWidget(self.detail_promo_position_widget) # # 등록모드 상세페이지 수정 토글 # self.watermark_widget = QWidget() # self.watermark_toggle_layout = QHBoxLayout(self.watermark_widget) # self.watermark_toggle_label = QLabel("상페 워터마크", self) # self.watermark_toggle = ToggleSwitch(self) # self.watermark_toggle.setObjectName("watermark_toggle") # self.watermark_toggle.clicked.connect(lambda checked: self.universal_input_handler(self.watermark_toggle, checked)) # self.watermark_widget.enterEvent = lambda e: self.show_manual_html( # self.detail_manual_group, # "💧 상세페이지 워터마크", # self.detail_manual_label, # """ #

상세페이지 이미지가 번역될 경우 워터마크를 추가하여 브랜드 인지도를 높입니다.

#

워터마크 텍스트와 투명도를 설정할 수 있습니다.

#

이미지 무단 사용을 방지하고 브랜드 아이덴티티를 강화합니다.

#

워터마크는 30도 각도로 회전하여 번역된 이미지의 전체에 반투명하게 배치됩니다.

# """ # ) # self.watermark_widget.leaveEvent = lambda e: self.reset_manual(self.detail_manual_group, self.detail_manual_label) # self.watermark_toggle_layout.addWidget(self.watermark_toggle_label) # self.watermark_toggle_layout.addWidget(self.watermark_toggle) # self.detail_toggle_layout.addWidget(self.watermark_widget) # 레이아웃에 그룹 추가 self.detail_layout.addWidget(self.detail_toggle_group, 3) self.detail_layout.addWidget(self.detail_manual_group, 7) # 레이아웃 추가 self.toggle_tab_widget.addTab(self.detail_tab, "상세페이지") return self.detail_tab def create_etc_tab(self): """기타 설정 탭을 생성합니다.""" self.etc_tab = QWidget() self.etc_layout = QHBoxLayout(self.etc_tab) # 토글 버튼 그룹 self.etc_toggle_group = QGroupBox("기능") self.etc_toggle_layout = QVBoxLayout(self.etc_toggle_group) self.set_group_style(self.etc_toggle_group, self.etc_toggle_layout, "neumorphism") self.etc_toggle_layout.setSpacing(10) # 설명 그룹 self.etc_manual_group = QGroupBox("매뉴얼") self.etc_manual_layout = QVBoxLayout(self.etc_manual_group) self.set_group_style(self.etc_manual_group, self.etc_manual_layout, "modern") self.etc_manual_layout.setContentsMargins(1, 1, 1, 1) self.etc_manual_layout.setSpacing(1) self.etc_manual_label = QTextEdit(self) self.etc_manual_label.setReadOnly(True) # 읽기 전용으로 설정 self.etc_manual_label.setWordWrapMode(QTextOption.NoWrap) self.etc_manual_label.setAcceptRichText(True) # HTML 지원 self.etc_manual_label.setLineWrapMode(QTextEdit.WidgetWidth) self.etc_manual_label.setWordWrapMode(QTextOption.WrapAtWordBoundaryOrAnywhere) # ↓ 스크롤바 정책: 세로는 필요할 때, 가로는 항상 숨김 self.etc_manual_label.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded) self.etc_manual_label.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) self.etc_manual_label.setStyleSheet(""" QTextEdit { background-color: white; border: 1px solid #e0e0e0; border-radius: 4px; padding: 8px; line-height: 1.4; color: #333; } """) self.reset_manual(self.etc_manual_group, self.etc_manual_label) self.etc_manual_layout.addWidget(self.etc_manual_label) # 토글 버튼 생성 # 디스코드 알림 토글 self.discord_widget = QWidget() self.discord_toggle_layout = QHBoxLayout(self.discord_widget) self.discord_notify_toggle_label = QLabel("디스코드 알림", self) self.discord_notify_toggle = ToggleSwitch(self) self.discord_notify_toggle.setObjectName("discord_notify_toggle") self.discord_notify_toggle.clicked.connect(self.update_discord_settings) self.discord_notify_toggle.clicked.connect(lambda checked: self.universal_input_handler(self.discord_notify_toggle, checked)) self.discord_notify_toggle.setToolTip("디스코드 알림 토글") self.discord_widget.enterEvent = lambda e: self.show_manual_html( self.etc_manual_group, "📢 디스코드 알림", self.etc_manual_label, """

작업 시작 및 완료 , 오류 발생 시 디스코드로 알림을 전송합니다.

모바일에서 작업 진행 상황을 모니터링할 수 있습니다.

웹훅 URL을 설정하여 원하는 디스코드 채널로 알림을 받을 수 있습니다.

""" ) self.discord_widget.leaveEvent = lambda e: self.reset_manual(self.etc_manual_group, self.etc_manual_label) self.discord_toggle_layout.addWidget(self.discord_notify_toggle_label) self.discord_toggle_layout.addWidget(self.discord_notify_toggle) self.etc_toggle_layout.addWidget(self.discord_widget) # 웹훅 URL 입력 필드 self.webhook_widget = QWidget() self.webhook_layout = QHBoxLayout(self.webhook_widget) self.webhook_label = QLabel("웹훅 URL:", self) self.webhook_input = QLineEdit() self.webhook_input.setObjectName("webhook_input") self.webhook_input.setText(self.settings_manager.get_value("webhook_input", "")) self.webhook_input.setPlaceholderText("디스코드 웹훅 URL을 입력하세요") self.webhook_input.textChanged.connect(lambda text: self.universal_input_handler(self.webhook_input, text)) self.webhook_widget.enterEvent = lambda e: self.show_manual_html( self.etc_manual_group, "🔗 디스코드 웹훅 URL", self.etc_manual_label, """

디스코드 알림을 받을 채널의 웹훅 URL을 입력합니다.

웹훅 생성 방법:

  1. 디스코드 채널 설정 > 웹훅 > 새 웹훅
  2. 웹훅 이름과 채널을 설정
  3. 웹훅 URL 복사하여 이곳에 붙여넣기

URL 형식: https://discord.com/api/webhooks/...

""" ) self.webhook_widget.leaveEvent = lambda e: self.reset_manual(self.etc_manual_group, self.etc_manual_label) self.webhook_layout.addWidget(self.webhook_label) self.webhook_layout.addWidget(self.webhook_input) self.etc_toggle_layout.addWidget(self.webhook_widget) # 레이아웃에 그룹 추가 self.etc_layout.addWidget(self.etc_toggle_group, 3) self.etc_layout.addWidget(self.etc_manual_group, 7) # GPT 모델 선택 드롭박스 self.gpt_model_widget = QWidget() self.gpt_model_layout = QHBoxLayout(self.gpt_model_widget) self.gpt_model_label = QLabel("[실험실]AI 모델 - 최신 모델 적용 중", self) self.gpt_model_label.setObjectName("gpt_model_label") self.gpt_model_combo = QComboBox(self) self.gpt_model = self.gpt_model_combo self.gpt_model_combo.setObjectName("gpt_model") # ⬇️ 사용자에게 보여줄 라벨과 내부 코드 매핑 # VIP 전용 Grok 모델 추가 gpt_items = [ ("GPT4o", "gpt-4o-mini"), ("GPT4.1", "gpt-4.1-nano"), ("GPT5", "gpt-5-nano"), ("🚀 Grok4 (VIP)", "grok-4-1-fast-reasoning"), # VIP 전용 ] self.gpt_model_combo.clear() for label, code in gpt_items: # label은 표시용, code는 내부 userData self.gpt_model_combo.addItem(label, code) # 기본 선택: 내부코드로 지정 default_code = "gpt-5-nano" idx = self.gpt_model_combo.findData(default_code) self.gpt_model_combo.setCurrentIndex(idx if idx >= 0 else 0) # 변경 신호: "텍스트" 대신 "코드(userData)"를 넘기도록 래핑 self.gpt_model_combo.currentIndexChanged.connect( lambda i: self.on_gpt_model_combo_changed(self.gpt_model_combo.itemData(i)) ) self.gpt_model_widget.enterEvent = lambda e: self.show_manual_html( self.etc_manual_group, "🔄 AI 모델 선택", self.etc_manual_label, """

AI 모델을 선택합니다:

""" ) self.gpt_model_widget.leaveEvent = lambda e: self.reset_manual(self.etc_manual_group, self.etc_manual_label) self.gpt_model_layout.addWidget(self.gpt_model_label) self.gpt_model_layout.addWidget(self.gpt_model_combo) self.etc_toggle_layout.addWidget(self.gpt_model_widget) # 자체서버 종류 선택 드롭박스 self.requests_server_type_widget = QWidget() self.requests_server_type_layout = QHBoxLayout(self.requests_server_type_widget) self.requests_server_type_label = QLabel("자체서버 종류", self) self.requests_server_type_label.setObjectName("requests_server_type_label") self.requests_server_type_combo = QComboBox(self) self.requests_server_type_combo.setObjectName("requests_server_type") # ⬇️ 사용자에게 보여줄 라벨과 내부 코드 매핑 server_type_items = [ ("메인서버", "main"), ("테스트서버", "test"), ] self.requests_server_type_combo.clear() for label, code in server_type_items: # label은 표시용, code는 내부 userData self.requests_server_type_combo.addItem(label, code) # 기본 선택: 내부코드로 지정 default_code = "main" idx = self.requests_server_type_combo.findData(default_code) self.requests_server_type_combo.setCurrentIndex(idx if idx >= 0 else 0) # 변경 신호: "텍스트" 대신 "코드(userData)"를 넘기도록 래핑 self.requests_server_type_combo.currentIndexChanged.connect( lambda i: self.on_requests_server_type_combo_changed(self.requests_server_type_combo.itemData(i)) ) self.requests_server_type_widget.enterEvent = lambda e: self.show_manual_html( self.etc_manual_group, "🔄 자체서버 종류 선택", self.etc_manual_label, """

자체서버의 종류를 선택합니다:

메인서버: 실제 운영용 메인 서버

테스트서버: 개발 및 테스트용 서버

서버 종류에 따라 연결되는 API 엔드포인트가 달라집니다.

""" ) self.requests_server_type_widget.leaveEvent = lambda e: self.reset_manual(self.etc_manual_group, self.etc_manual_label) self.requests_server_type_layout.addWidget(self.requests_server_type_label) self.requests_server_type_layout.addWidget(self.requests_server_type_combo) self.etc_toggle_layout.addWidget(self.requests_server_type_widget) # 레이아웃 추가 self.toggle_tab_widget.addTab(self.etc_tab, "기타 설정") return self.etc_tab def create_login_group(self): """로그인 그룹을 생성합니다.""" self.admin_group_layout = QVBoxLayout() # 관리자 토글 self.admin_toggle = ToggleSwitch(self) # self.admin_toggle.clicked.connect(self.on_admin_toggle_clicked) self.admin_toggle.clicked.connect(lambda checked: self.universal_input_handler(self.admin_toggle, checked)) self.admin_toggle.setToolTip("관리자 여부 설정") self.admin_toggle.setObjectName("admin_toggle") # 관리자 토글 영역 self.admin_toggle_layout = QHBoxLayout() self.admin_toggle_layout.addWidget(QLabel("관리자 여부:", self)) self.admin_toggle_layout.addWidget(self.admin_toggle) self.admin_group_layout.addLayout(self.admin_toggle_layout) # 관리자 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.user_id_label = QLabel("직원 ID:", self) self.user_id_input = QLineEdit(self) self.user_id_input.setObjectName("user_id_input") self.user_pw_label = QLabel("직원 PW:", self) self.user_pw_input = QLineEdit(self) self.user_pw_input.setObjectName("user_pw_input") self.user_pw_input.setEchoMode(QLineEdit.Password) self.user_pw_input.setPlaceholderText("직원 PW 입력") self.user_id_input.setPlaceholderText("직원 ID 입력") self.user_id_input.textChanged.connect(lambda text: self.universal_input_handler(self.user_id_input, text)) self.user_pw_input.textChanged.connect(lambda text: self.universal_input_handler(self.user_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.user_id_layout = QHBoxLayout() self.user_id_layout.addWidget(self.user_id_label) self.user_id_layout.addWidget(self.user_id_input) self.admin_group_layout.addLayout(self.user_id_layout) self.user_pw_layout = QHBoxLayout() self.user_pw_layout.addWidget(self.user_pw_label) self.user_pw_layout.addWidget(self.user_pw_input) self.admin_group_layout.addLayout(self.user_pw_layout) # 왼쪽: 관리자 토글 및 ID/PW 그룹 self.admin_group = QGroupBox("관리자/직원 로그인") self.admin_group.setStyleSheet(self.get_modern_groupbox_style("#3498db", "#2980b9")) 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(self.get_modern_groupbox_style("#27ae60", "#229954")) self.user_info_layout = QGridLayout() # VBox에서 Grid로 변경하여 2열 배치 # 사용자 상세 정보 추가 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: 2px; font-size: 9pt; """ 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) # 2열 그리드로 정보 배치 self.user_info_layout.addWidget(self.user_name_label, 0, 0) self.user_info_layout.addWidget(self.user_email_label, 0, 1) self.user_info_layout.addWidget(self.user_sessions_label, 1, 0) self.user_info_layout.addWidget(self.user_membership_label, 1, 1) self.user_info_layout.addWidget(self.user_version_label, 2, 0) # 만료일 정보는 전체 너비로 expiry_widget = QWidget() expiry_widget.setLayout(expiry_layout) self.user_info_layout.addWidget(expiry_widget, 2, 1) self.user_info_group.setLayout(self.user_info_layout) self.user_info_group.setFixedHeight(170) # 기존 높이로 복원하여 내용 전체 표시 return self.user_info_group def set_button_style(self, button, theme, width, height): button.setFixedWidth(width) button.setFixedHeight(height) # 테마별 색상 매핑 theme_map = { "blue": "blue", "yellow": "yellow", "green": "green", "red": "red", "purple": "purple", "gray": "blue" # gray는 blue로 매핑 } color_scheme = theme_map.get(theme, "blue") button.setStyleSheet(self.get_modern_button_style(color_scheme)) def create_Settings_buttons(self): """버튼 그룹을 생성합니다.""" setting_buttons_group = QGroupBox("설정") setting_buttons_group.setStyleSheet(self.get_modern_groupbox_style("#f39c12", "#e67e22")) setting_buttons_layout = QHBoxLayout() # 설정열기 버튼 추가 self.toggle_settings_button = QPushButton("편집설정 열기", self) self.set_button_style(self.toggle_settings_button, "yellow", 140, 60) self.toggle_settings_button.clicked.connect(self.toggle_settings_visibility) setting_buttons_layout.addWidget(self.toggle_settings_button) # 금지어 버튼 추가 self.forbbidenWord_button = QPushButton('금지어', self) self.set_button_style(self.forbbidenWord_button, "yellow", 140, 60) self.forbbidenWord_button.clicked.connect(self.on_forbbidenWord_button_clicked) setting_buttons_layout.addWidget(self.forbbidenWord_button) # # 매뉴얼 버튼 추가 # self.manual_button = QPushButton('매뉴얼', self) # self.set_button_style(self.manual_button, "yellow", 120, 50) # self.manual_button.clicked.connect(self.on_manual_button_clicked) # setting_buttons_layout.addWidget(self.manual_button) # 로그 버튼 추가 self.log_button = QPushButton('로그', self) self.set_button_style(self.log_button, "yellow", 140, 60) self.log_button.clicked.connect(self.show_log_dialog) setting_buttons_layout.addWidget(self.log_button) # 확장설치 버튼 추가 self.extension_install_button = QPushButton('🧩 확장\n설치', self) self.set_button_style(self.extension_install_button, "gray", 140, 60) self.extension_install_button.clicked.connect(self.on_extension_install_button_clicked) setting_buttons_layout.addWidget(self.extension_install_button) # # 등록상품모드 버튼은 토글 설정 내부로 이동됨 # # GPU 상태 확인 버튼 추가 (프리미엄 이상 사용자만) # if self.user_membership_level in ['premium', 'vip', 'admin']: # self.gpu_status_button = QPushButton('🎮 GPU 상태\n모드활성화', self) # self.set_button_style(self.gpu_status_button, "green", 140, 60) # self.gpu_status_button.clicked.connect(self.on_gpu_status_button_clicked) # setting_buttons_layout.addWidget(self.gpu_status_button) # self.logger.log(f"GPU 상태 버튼 활성화 - 멤버십 레벨: {self.user_membership_level}", level=logging.DEBUG) # else: # self.gpu_status_button = None # self.logger.log(f"GPU 상태 버튼 비활성화 - 멤버십 레벨: {self.user_membership_level} (프리미엄 이상 필요)", level=logging.DEBUG) # 이미지 프로세서 관리 버튼 추가 self.image_processor_button = QPushButton('🖼️ 이미지\n프로세서', self) self.set_button_style(self.image_processor_button, "blue", 140, 60) self.image_processor_button.clicked.connect(self.on_image_processor_button_clicked) setting_buttons_layout.addWidget(self.image_processor_button) self.logger.log("이미지 프로세서 버튼 활성화", level=logging.DEBUG) setting_buttons_group.setLayout(setting_buttons_layout) return setting_buttons_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() # 크기 정책 설정: 로그인 그룹은 1/3, 사용자 정보 그룹은 2/3 # login_group.setMaximumWidth(230) # 로그인 그룹 크기 제한 login_group.setFixedHeight(170) # 로그인 그룹 높이를 사용자 정보와 맞춤 user_info_group.setMinimumWidth(350) # 사용자 정보 그룹 최소 크기 # 레이아웃에 추가 (비율 설정) self.login_info_layout.addWidget(login_group, 2) # 비율 1 self.login_info_layout.addWidget(user_info_group, 1) # 비율 2 return self.login_info_layout def create_select_group(self): self.select_group = QGroupBox("작업 그룹") self.select_group.setStyleSheet(self.get_modern_groupbox_style("#27ae60", "#229954")) # 그룹 선택 드롭박스 및 툴팁 추가 self.select_group_layout = QGridLayout() # 그룹 선택 드롭박스 및 툴팁 추가 self.group_selector_label = QLabel("그룹 선택:", self) self.group_selector_label.setFixedHeight(30) self.group_selector_label.setStyleSheet(self.get_modern_label_style("#2c3e50", "transparent")) self.group_selector = QComboBox(self) self.group_selector.setFixedHeight(30) # self.group_selector.setFixedWidth(120) # 드롭박스 크기를 절반으로 self.group_selector.setStyleSheet(self.get_modern_input_style()) self.group_selector.setToolTip( "직원계정은 3개, 관리자계정은 20개 중 선택할 수 있습니다.\n해당 그룹이 없을 경우 기본으로 1번그룹을 작업합니다." ) self.group_selector.currentIndexChanged.connect(self.on_group_selected) self.group_selector.setEnabled(False) # 그룹 이름 표시 QLabel self.selected_group_label = QLabel("선택된 그룹: ", self) self.selected_group_label.setFixedHeight(30) self.selected_group_label.setStyleSheet(self.get_modern_label_style("#2c3e50", "transparent")) self.selected_group = QLabel("없음", self) self.selected_group.setFixedHeight(30) # self.selected_group.setFixedWidth(80) # 선택된 그룹 라벨 크기 절반으로 self.selected_group.setAlignment(Qt.AlignCenter) # 가운데 정렬 self.selected_group.setStyleSheet(self.get_modern_label_style("#2c3e50", "#e2e8f0")) # 기본 상태는 직원 (3개 그룹) self.update_group_items(is_admin=False) self.selected_group_total_products = QLabel("", self) self.selected_group_total_products.setFixedHeight(30) self.selected_group_total_products.setAlignment(Qt.AlignCenter) # 가운데 정렬 self.selected_group_total_products.setStyleSheet(self.get_modern_label_style("#2c3e50", "#e2e8f0")) # 세션 정리 버튼 추가 self.session_cleanup_button = QPushButton("세션 정리", self) self.session_cleanup_button.setFixedHeight(30) self.session_cleanup_button.clicked.connect(self.on_session_cleanup_button_clicked) self.session_cleanup_button.setToolTip("비활성 세션(1분 이상 활동 없음)을 정리합니다.\n정리 후 그룹 목록이 자동으로 새로고침됩니다.") self.session_cleanup_button.setStyleSheet(self.get_modern_button_style("orange")) self.session_cleanup_button.setEnabled(True) # 로그인 후 항상 활성화 # 그룹 Refresh 버튼 추가 self.group_refresh_button = QPushButton("그룹 새로고침", self) self.group_refresh_button.setFixedHeight(30) self.group_refresh_button.clicked.connect(self.on_group_refresh_button_clicked) self.group_refresh_button.setToolTip("작업 그룹 목록을 서버에서 다시 가져옵니다.") self.group_refresh_button.setStyleSheet(self.get_modern_button_style("green")) self.group_refresh_button.setEnabled(False) # 브라우저 시작 후 활성화 self.group_change_button = QPushButton("그룹 선택", self) self.group_change_button.setFixedHeight(30) # self.group_change_button.setFixedWidth(120) # 드롭박스 크기 줄인 만큼 버튼 크기 확대 self.group_change_button.clicked.connect(self.on_group_change_button_clicked) self.group_change_button.setToolTip("그룹 선택 후 버튼을 눌러야 작업그룹이 변경됩니다. 기본값은 1번째 그룹입니다\n(상품편집 시작전에는 재설정도 가능합니다.)") self.group_change_button.setStyleSheet(self.get_modern_button_style("blue")) # 기존 set_button_style 대신 직접 스타일 설정 self.group_change_button.setEnabled(False) self.select_group_layout.addWidget(self.group_selector_label, 0, 0) self.select_group_layout.addWidget(self.group_selector, 0, 1) self.select_group_layout.addWidget(self.session_cleanup_button, 0, 2) self.select_group_layout.addWidget(self.group_refresh_button, 0, 3) self.select_group_layout.addWidget(self.group_change_button, 0, 4) self.select_group_layout.addWidget(self.selected_group_label, 2, 0) self.select_group_layout.addWidget(self.selected_group, 2, 1) self.select_group_layout.addWidget(self.selected_group_total_products, 2, 2) self.select_group.setLayout(self.select_group_layout) # return self.select_group_layout return self.select_group def create_job_group(self): self.job_group = QGroupBox("편집 작업") self.job_group.setStyleSheet(self.get_modern_groupbox_style("#3498db", "#2980b9")) self.job_group_layout = QGridLayout() # 크롬 실행 버튼 및 번역 버튼 self.start_chrome_button = QPushButton('편집알바생\n로그인', self) self.start_chrome_button.setToolTip("크롬 브라우저를 실행하여 알바생 로그인 후 신규수집상품 페이지의 작업그룹을 선택합니다\n작업그룹 선택에 오류가 있을경우 프로그램을 재실행해 주세요.") self.start_chrome_button.setObjectName("start_chrome_button") self.start_chrome_button.clicked.connect(self.start_browser_thread) self.PercentyJob_button = QPushButton('상품편집\n시작', self) self.PercentyJob_button.setToolTip("신규수집 상품페이지에서 선택된 그룹의 상품목록 전체를 편집합니다") self.PercentyJob_button.setEnabled(False) self.PercentyJob_button.clicked.connect(self.on_start_PercentyJob_clicked) # self.pause_button = QPushButton('일시정지', self) # self.pause_button.setToolTip("상품편집 일시정지\n일시정지버튼을 누를 경우 해당 스테이지가 완료된 후 일시정지되며 프로그레스바가 비활성화됩니다") # self.pause_button.setEnabled(False) # self.pause_button.clicked.connect(self.on_pause_button_clicked) self.set_button_style(self.start_chrome_button, "blue", 150, 60) self.set_button_style(self.PercentyJob_button, "blue", 150, 60) # (등록모드+VIP 전용) 사업자 변경 버튼 추가 - 초기 비활성/비가시 try: # 마켓정보변경 버튼 추가 self.market_change_button = QPushButton('마켓API변경', self) self.set_button_style(self.market_change_button, "orange", 150, 60) self.market_change_button.setToolTip("선택된 사업자의 마켓 API 정보를 변경하여 다양한 사업자로 상품을 업로드합니다.") self.market_change_button.setEnabled(False) self.market_change_button.setVisible(False) self.market_change_button.clicked.connect(self.on_market_change_button_clicked) self.remove_upload_info_button = QPushButton('현재그룹\n업로드정보삭제', self) self.set_button_style(self.remove_upload_info_button, "red", 150, 60) self.remove_upload_info_button.setToolTip("업로드된 상품 정보를 삭제합니다.") self.remove_upload_info_button.setEnabled(False) self.remove_upload_info_button.setVisible(False) self.remove_upload_info_button.clicked.connect(self.on_remove_upload_info_clicked) self.upload_selected_markets_button = QPushButton('현재그룹\n업로드', self) self.set_button_style(self.upload_selected_markets_button, "violet", 150, 60) self.upload_selected_markets_button.setToolTip("현재 그룹의 상품을 업로드합니다.\n(사업자 관리에서 설정된 작업순서에 따라 순차적으로 업로드합니다)") self.upload_selected_markets_button.setEnabled(False) self.upload_selected_markets_button.setVisible(False) self.upload_selected_markets_button.clicked.connect(self.on_upload_selected_markets_clicked) self.upload_selected_markets_Multi_button = None except Exception: self.remove_upload_info_button = None self.market_change_button = None self.upload_selected_markets_button = None self.upload_selected_markets_Multi_button = None # self.set_button_style(self.pause_button, "blue", 150, 60) self.job_group_layout.addWidget(self.start_chrome_button, 0, 0, 1, 1) if self.market_change_button is not None: self.job_group_layout.addWidget(self.market_change_button, 0, 1, 1, 1) if hasattr(self, 'remove_upload_info_button') and self.remove_upload_info_button is not None: self.job_group_layout.addWidget(self.remove_upload_info_button, 0, 2, 1, 1) self.job_group_layout.addWidget(self.PercentyJob_button, 0, 3, 1, 1) self.job_group_layout.addWidget(self.upload_selected_markets_button, 0, 4, 1, 1) # self.job_group_layout.addWidget(self.upload_selected_markets_Multi_button, 0, 5, 1, 1) else: self.job_group_layout.addWidget(self.PercentyJob_button, 0, 3, 1, 1) else: self.job_group_layout.addWidget(self.PercentyJob_button, 0, 2, 1, 1) # self.job_group_layout.addWidget(self.pause_button, 0, 2, 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(self.get_modern_progressbar_style("blue")) # 스테이지 타임라인 self.stageTimeline_layout = QHBoxLayout() self.stages = ["상품명", "옵션", "가격", "태그", "썸네일","상페"] self.stage_labels = [] for stage in self.stages: # self.stage_layout = QHBoxLayout() label = QLabel(stage) label.setStyleSheet(self.get_modern_label_style("#2c3e50", "#e2e8f0")) 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(self.get_modern_progressbar_style("blue")) 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_display.setStyleSheet(self.get_modern_input_style()) 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(600, 30, 600, 1000)) # 더 넓고 높게 조정 self.setWindowTitle('편집알바생') # 설정 초기화 self.init_settings() # 메뉴바 생성 self.create_menu_bar() # 관리자 레이아웃 생성 self.admin_layout = self.create_admin_layout() self.main_layout.addLayout(self.admin_layout) # 선택 그룹 레이아웃 생성 self.select_group_widget = self.create_select_group() self.main_layout.addWidget(self.select_group_widget) # 편집 작업 레이아웃 생성 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.memory_monitor_group = self.create_memory_monitor_group() self.main_layout.addWidget(self.memory_monitor_group) # 토글 레이아웃 생성 toggle_main_widget = self.creat_Toggle_tab() toggle_main_widget.setVisible(False) self.main_layout.addWidget(toggle_main_widget) # 초기 실행 시 기본모드 가시성 강제 적용 try: QTimer.singleShot(0, self._apply_initial_register_mode_visibility) except Exception: pass # 프로그레스 레이아웃 생성 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) # GPU 초기화 완료 후 드롭다운 옵션 업데이트 # QTimer.singleShot(1000, self.update_translation_engine_options_after_gpu_init) def update_translation_engine_options_after_gpu_init(self): """GPU 초기화 완료 후 번역 엔진 드롭다운 옵션 업데이트""" try: self.logger.log("GPU 초기화 완료 후 번역 엔진 옵션 업데이트 시작", level=logging.INFO) # GPU 매니저 초기화 (아직 초기화되지 않은 경우) if not hasattr(self, 'gpu_manager_for_ui') or not self.gpu_manager_for_ui: self.gpu_manager_for_ui = GPUManager(logger=self.logger) self.logger.log("GPU 매니저 새로 생성됨", level=logging.INFO) # GPU 상태 초기화 및 확인 self.gpu_manager_for_ui.initialize_gpu_state(self.toggle_states) self.logger.log(f"GPU 상태 초기화 완료: can_use_cuda={self.gpu_manager_for_ui.can_use_cuda}", level=logging.INFO) # 번역 엔진은 ImageProcessorDialog에서 통합 관리 # 더 이상 개별 콤보박스 업데이트 불필요 except Exception as e: self.logger.log(f"번역 엔진 옵션 업데이트 중 오류 발생: {e}", level=logging.ERROR, exc_info=True) def update_stage_timeline(self, active_stages: list): """ active_stages: 사용자가 선택한 작업 항목의 리스트 (예: ["상품명", "옵션"]) """ # 기존 레이아웃의 모든 위젯 제거 try: 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(self.get_modern_label_style("#2c3e50", "#e2e8f0")) self.stage_labels.append(label) self.stageTimeline_layout.addWidget(label) except Exception as e: self.logger.log(f"스테이지 타임라인 업데이트 중 오류 발생: {e}", level=logging.ERROR, exc_info=True) 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 toggle_settings_visibility(self): """설정열기 버튼 클릭 시 토글 탭 위젯 표시/숨김 처리""" current_visible = self.toggle_main_widget.isVisible() new_visible = not current_visible # 버튼 텍스트 변경 self.toggle_settings_button.setText("편집설정닫기" if new_visible else "편집설정열기") # 설정 컨테이너 가시성 설정 self.toggle_main_widget.setVisible(new_visible) # 등록상품모드일 때 탭 가시성 동기화 if new_visible: try: self.apply_register_mode_tab_visibility() self.apply_register_mode_widget_visibility() except Exception as e: self.logger.log(f"등록상품모드 탭 가시성 적용 중 오류: {e}", level=logging.WARNING) # 그룹 선택 위젯 가시성 설정 self.select_group_widget.setVisible(not new_visible) self.job_group_widget.setVisible(not new_visible) # # 기존 toggle_layout_widget도 일관성을 위해 같이 처리 # self.toggle_main_widget.setVisible(new_visible) # log_display 가시성 설정 (설정 열기면 로그 숨김, 설정 닫기면 로그 표시) self.log_display.setVisible(not new_visible) # 창 크기 조정 if new_visible: # QApplication 업데이트하여 정확한 위젯 크기 계산 self.toggle_main_widget.adjustSize() QApplication.processEvents() # 설정 컨테이너의 실제 높이 계산 settings_height = self.toggle_main_widget.height() # 현재 창 크기 가져오기 current_size = self.size() # 화면 크기 고려 screen_height = QGuiApplication.primaryScreen().availableGeometry().height() max_height = screen_height * 0.8 # 화면 높이의 80%로 제한 # 새 높이 계산 (화면 제약 고려) new_height = min(current_size.height() + settings_height, max_height) # 새 크기 설정 self.resize(current_size.width(), new_height) else: # 설정 패널 닫을 때 창 크기 원래대로 복원 self.adjustSize() # 로그 기록 self.logger.log(f"설정 패널 {('표시' if new_visible else '숨김')}", level=logging.DEBUG) # def on_lens_toggle_clicked(self, is_checked): # """렌즈 토글 상태에 따라 API사용 토들 표시/숨김""" # if is_checked: # self.use_API_toggle.setVisible(True) # else: # self.use_API_toggle.setVisible(False) # def on_vd_mode_for_detail_imageTrans_clicked(self, is_checked): # """상페이미지 번역여부에 따라 VD 모드 선택 필드를 표시/숨김""" # if is_checked: # self.vd_mode_toggle.setVisible(True) # self.vd_mode_toggle_label.setVisible(True) # else: # self.vd_mode_toggle.setVisible(False) # self.vd_mode_toggle_label.setVisible(False) 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 on_pause_button_clicked(self): # """일시정지 버튼 클릭 시 호출""" # self.logger.log("일시정지 버튼 클릭됨", level=logging.INFO) # if self.pause_button.text() == "일시정지": # # 일시정지 기능 # self.pause_button.setText("재개") # self.pause_button.setStyleSheet(""" # QPushButton { # background-color: #F5F5F5; # box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); # color: black; # border: 1px solid #111110; # border-radius: 4px; # padding: 8px 15px; # min-width: 100px; # } # QPushButton:hover { # background-color: #E0E0E0; # } # """) # # 일시정지 안내 팝업 메시지 표시 # self.pause_message_box = QMessageBox(self) # self.pause_message_box.setIcon(QMessageBox.Information) # self.pause_message_box.setWindowTitle("일시정지 요청") # self.pause_message_box.setText("잠시 후 스테이지가 완료되면 일시정지됩니다") # self.pause_message_box.setStandardButtons(QMessageBox.NoButton) # 버튼 없음 # self.pause_message_box.setModal(False) # 모달리스 대화상자로 설정 # self.pause_message_box.show() # # 3초 후 메시지 박스 자동 종료를 위한 타이머 설정 # self.pause_timer = QTimer(self) # self.pause_timer.setSingleShot(True) # 한 번만 실행 # self.pause_timer.timeout.connect(self.pause_message_box.close) # 타이머 종료 시 메시지 박스 닫기 # self.pause_timer.start(3000) # 3초(3000ms) 후 실행 # # 작업 일시정지 설정 # self.is_paused = True # # 브라우저 컨트롤러에 일시정지 상태 전달 # if hasattr(self, 'browser_controller') and self.browser_controller: # # 시그널 연결 - 일시정지 확인 시 팝업 닫기 # self.browser_controller.pause_confirmed.connect(self.close_pause_message) # self.browser_controller.pause_task() # # 상태 메시지 표시 # self.detail_progress_bar.setFormat("작업 일시정지됨") # self.logger.log("작업이 일시정지되었습니다.", level=logging.INFO) # # 디스코드 알림 (활성화된 경우에만) # if self.discord_notify_toggle.isChecked(): # self.discord_manager.send_notification("⏸️ 작업이 일시정지되었습니다.") # else: # # 재개 기능 # self.pause_button.setText("일시정지") # self.set_button_style(self.pause_button, "blue", 80, 40) # # 작업 재개 설정 # self.is_paused = False # # 브라우저 컨트롤러에 재개 상태 전달 # if hasattr(self, 'browser_controller') and self.browser_controller: # # 시그널 연결 해제 # if hasattr(self.browser_controller, 'pause_confirmed'): # self.browser_controller.pause_confirmed.disconnect(self.close_pause_message) # self.browser_controller.resume_task() # # 상태 메시지 업데이트 # self.detail_progress_bar.setFormat("작업 재개됨") # self.logger.log("작업이 재개되었습니다.", level=logging.INFO) # # 디스코드 알림 (활성화된 경우에만) # if self.discord_notify_toggle.isChecked(): # self.discord_manager.send_notification("▶️ 작업이 재개되었습니다.") # 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_gpt_model_changed(self, idx): # # 사용자가 선택한 값과 label # selected_value = self.gpt_model_combo.currentData() # selected_label = self.gpt_model_combo.currentText() # # 선택 모델의 min_level 구하기 # for model in GPT_MODELS: # if model["value"] == selected_value: # min_level = model["min_level"] # break # # 만약 사용자가 등급보다 높은 모델 선택 시 # if MEMBERSHIP_LEVELS.index(self.user_level) < MEMBERSHIP_LEVELS.index(min_level): # QMessageBox.warning(self, "권한 부족", f"{selected_label} 모델은 현재 등급({self.user_level})에서 사용할 수 없습니다.") # # 본인 등급에서 선택 가능한 최상위 모델로 롤백 # for i in reversed(range(self.gpt_model_combo.count())): # allowed_value = self.gpt_model_combo.itemData(i) # # 본인 등급 이하만 # for model in GPT_MODELS: # if model["value"] == allowed_value and MEMBERSHIP_LEVELS.index(self.user_level) >= MEMBERSHIP_LEVELS.index(model["min_level"]): # self.gpt_model_combo.setCurrentIndex(i) # return # def on_cmb_test_button_clicked(self, test_cat): # """크무비 설정 실행 버튼 클릭 시 호출""" # self.logger.log('크무비 테스트 버튼 클릭됨', level=logging.DEBUG) # text, ok = QInputDialog.getText(self, "카테고리 입력 테스트", "카테고리를 형식에 맞게 입력하세요:") # if ok and text: # 사용자가 확인 버튼을 누르고 텍스트를 입력한 경우 # stage = self.cmb_diag.get_crmobi_stage(text) # self.logger.log(f"{stage}", level=logging.DEBUG) def on_forbbidenWord_button_clicked(self): """금지어 관리 버튼 클릭 시 호출""" self.logger.log("금지어 관리 버튼 클릭됨", level=logging.DEBUG) self.keyword_manager.exec() def on_gpu_status_button_clicked(self): """GPU 상태 확인 버튼 클릭 시 호출 (프리미엄 이상 사용자만) - 실제 GPU 추론 테스트 포함""" self.logger.log("GPU 상태 확인 버튼 클릭됨 (실제 DirectML 테스트 포함)", level=logging.DEBUG) # 권한 재확인 if self.user_membership_level not in ['premium', 'vip', 'admin']: QMessageBox.warning( self, "권한 필요", "GPU 상태 확인은 프리미엄 이상 멤버십에서만 사용할 수 있습니다.\n\n" f"현재 멤버십: {self.user_membership_level}\n" "멤버십 업그레이드를 원하시면 관리자에게 문의해주세요." ) return try: # 1단계: 기본 GPU 정보 확인 gpu_info = self.get_gpu_info() # 2단계: 실제 DirectML 추론 테스트 수행 self.logger.log("🧪 실제 DirectML 추론 테스트 시작...", level=logging.DEBUG) # 프로그레스 다이얼로그 표시 progress = QProgressDialog("GPU 성능 테스트 중...", "취소", 0, 100, self) progress.setWindowModality(Qt.WindowModal) progress.setAutoClose(True) progress.setAutoReset(False) progress.setValue(10) progress.show() QApplication.processEvents() # GPU 관리자를 통한 실제 DirectML 테스트 from src.modules.gpu_utils import GPUManager gpu_manager = GPUManager(self.logger) # 실제 DirectML 종합 테스트 (추론 포함) directml_test_result = gpu_manager.test_directml_comprehensive() progress.hide() # 3단계: 테스트 결과에 따른 UI 표시 self._show_gpu_test_results(gpu_info, directml_test_result) except Exception as e: self.logger.log(f"GPU 상태 확인 중 오류: {e}", level=logging.ERROR, exc_info=True) QMessageBox.critical(self, "오류", f"GPU 상태 확인 중 오류가 발생했습니다:\n{e}") def _show_gpu_test_results(self, gpu_info, test_results): """GPU 테스트 결과를 사용자에게 표시""" msg_box = QMessageBox(self) msg_box.setWindowTitle("🎮 GPU 상태 및 실제 성능 테스트 결과") msg_box.setTextFormat(Qt.RichText) # 테스트 성공 여부에 따른 UI 구성 if test_results['inference_test_passed'] and test_results['directml_available']: # ✅ GPU 사용 가능 msg_box.setIcon(QMessageBox.Information) gpu_type_text = { 'integrated': '내장 GPU', 'dedicated': '외장 GPU', 'unknown': '알 수 없음' }.get(gpu_info['gpu_type'], '알 수 없음') vendor_text = { 'intel': 'Intel', 'amd': 'AMD', 'nvidia': 'NVIDIA', 'unknown': '알 수 없음' }.get(gpu_info['vendor'], '알 수 없음') vm_status = "🖥️ VM 환경 (GPU 패스스루 동작 중)" if test_results['vm_detected'] else "💻 물리 환경" test_duration = test_results.get('test_duration', 0) msg_box.setText(f"""

✅ GPU 가속 사용 가능 확인됨

🧪 실제 DirectML 추론 테스트: 성공

테스트 시간: {test_duration:.2f}초

감지된 GPU: {gpu_info.get('model_name', 'Unknown')}

벤더: {vendor_text} | 타입: {gpu_type_text}

환경: {vm_status}

📋 GPU 모드 사용 권장

모든 번역 설정에서 'GPU' 모드를 선택하세요!

🚀 실제 GPU 가속이 정상 동작합니다. 번역 속도가 크게 향상됩니다!

""") else: # ❌ GPU 사용 불가 msg_box.setIcon(QMessageBox.Warning) error_details = test_results.get('error_message', 'Unknown error') vm_warning = "" if test_results['vm_detected']: vm_warning = """

🖥️ VM 환경 감지됨

Proxmox, VMware, VirtualBox 등의 가상화 환경에서는
GPU 패스스루 설정이 필요할 수 있습니다.

""" msg_box.setText(f"""

❌ GPU 가속 사용 불가

🧪 DirectML 추론 테스트: 실패

{error_details}

{vm_warning}

📋 권장 설정

모든 번역 설정에서 'CPU' 모드를 사용하세요

가능한 원인:

안전한 CPU 모드로 동작하며, 품질은 동일합니다.

""") # 결과에 따른 추가 액션 버튼 if test_results['inference_test_passed']: # GPU 모드 자동 설정 버튼 추가 auto_gpu_button = msg_box.addButton("🚀 GPU 모드 자동 설정", QMessageBox.ActionRole) auto_gpu_button.clicked.connect(lambda: self._auto_set_gpu_mode()) else: # CPU 모드 자동 설정 버튼 추가 auto_cpu_button = msg_box.addButton("🔒 CPU 모드 자동 설정", QMessageBox.ActionRole) auto_cpu_button.clicked.connect(lambda: self._auto_set_cpu_mode()) msg_box.addButton("확인", QMessageBox.AcceptRole) msg_box.exec() def _auto_set_gpu_mode(self): """GPU 모드로 모든 설정 자동 변경""" try: gpu_settings = [ 'optionIMGTrans_type', 'detail_IMGTrans_type', 'thumb_trans_type' ] for setting in gpu_settings: if hasattr(self, setting + '_combo'): combo = getattr(self, setting + '_combo') combo.setCurrentText("GPU") if hasattr(self, 'toggle_states'): self.toggle_states[setting] = "GPU" # migan_use_cuda도 활성화 if hasattr(self, 'toggle_states'): self.toggle_states['migan_use_cuda'] = True self.toggle_states['use_cuda'] = True self.logger.log("🚀 모든 설정이 GPU 모드로 자동 변경됨", level=logging.INFO) QMessageBox.information(self, "설정 완료", "모든 번역 설정이 GPU 모드로 변경되었습니다!") except Exception as e: self.logger.log(f"GPU 모드 자동 설정 실패: {e}", level=logging.ERROR) def on_image_processor_button_clicked(self): """이미지 프로세서 관리 버튼 클릭 시 호출""" self.logger.log("이미지 프로세서 관리 버튼 클릭됨", level=logging.DEBUG) try: # 미리 생성된 이미지 프로세서 관리 다이얼로그 표시 # (이미 자동 초기화가 완료되었으므로 바로 표시) self.image_processor_dialog.exec() except Exception as e: self.logger.log(f"이미지 프로세서 관리 다이얼로그 열기 실패: {e}", level=logging.ERROR, exc_info=True) QMessageBox.critical(self, "오류", f"이미지 프로세서 관리 다이얼로그를 열 수 없습니다:\n{e}") def on_extension_install_button_clicked(self): """확장 프로그램 설치 버튼 클릭 시 호출""" self.logger.log("확장 프로그램 설치 버튼 클릭됨", level=logging.DEBUG) try: # Supabase에서 extension 정보 가져오기 if not self.supabase_manager: QMessageBox.warning(self, "오류", "서버 연결이 필요합니다.\n로그인 후 다시 시도해주세요.") return # 버튼 비활성화 (중복 클릭 방지) self.extension_install_button.setEnabled(False) self.extension_install_button.setText("🧩 다운로드\n중...") QApplication.processEvents() # program_versions 테이블에서 extension 정보 가져오기 extension_info = self.supabase_manager.client.table('program_versions') \ .select('*') \ .eq('program_id', 'extension') \ .execute() if not extension_info.data: self.logger.log("확장 프로그램 정보를 가져올 수 없습니다.", level=logging.ERROR) QMessageBox.warning(self, "오류", "확장 프로그램 정보를 가져올 수 없습니다.") self._reset_extension_button() return # 가장 최신 버전 선택 (여러 버전이 있을 경우) from packaging import version valid_versions = [] for ver_info in extension_info.data: try: ver = version.parse(ver_info['version']) valid_versions.append((ver, ver_info)) except Exception as ve: self.logger.log(f"버전 파싱 오류: {ver_info['version']}: {str(ve)}", level=logging.ERROR) if not valid_versions: self.logger.log("유효한 확장 프로그램 버전 정보가 없습니다.", level=logging.ERROR) QMessageBox.warning(self, "오류", "유효한 확장 프로그램 버전 정보가 없습니다.") self._reset_extension_button() return # 가장 높은 버전 선택 highest_version_info = max(valid_versions, key=lambda x: x[0])[1] download_url = highest_version_info.get('download_url') if not download_url: self.logger.log("다운로드 URL이 없습니다.", level=logging.ERROR) QMessageBox.warning(self, "오류", "다운로드 URL을 찾을 수 없습니다.") self._reset_extension_button() return self.logger.log(f"확장 프로그램 다운로드 시작: {download_url}", level=logging.INFO) # 파일 다운로드 import requests from urllib.parse import urlparse import tempfile import subprocess response = requests.get(download_url, stream=True) response.raise_for_status() # 파일명 추출 url_path = urlparse(download_url).path filename = os.path.basename(url_path) if not filename: filename = "extension_installer.exe" # 임시 디렉토리에 다운로드 from pathlib import Path temp_dir = Path(os.environ.get("TEMP", r"C:\Temp")) / "extension_installer" temp_dir.mkdir(exist_ok=True) download_path = temp_dir / filename # 다운로드 진행 total_size = int(response.headers.get('content-length', 0)) downloaded = 0 with open(download_path, 'wb') as f: for chunk in response.iter_content(chunk_size=8192): if chunk: f.write(chunk) downloaded += len(chunk) if total_size > 0: progress = int((downloaded / total_size) * 100) self.extension_install_button.setText(f"🧩 다운로드\n{progress}%") QApplication.processEvents() self.logger.log(f"확장 프로그램 다운로드 완료: {download_path}", level=logging.INFO) # 다운로드 완료 후 실행 self.extension_install_button.setText("🧩 설치\n중...") QApplication.processEvents() # 실행 파일인 경우 실행 if str(download_path).lower().endswith('.exe'): self.logger.log(f"확장 프로그램 실행: {download_path}", level=logging.INFO) subprocess.Popen([str(download_path)], shell=True) QMessageBox.information(self, "설치 시작", f"확장 프로그램 설치가 시작되었습니다.\n버전: {highest_version_info.get('version', 'Unknown')}") else: # exe가 아닌 경우 파일 위치 열기 os.startfile(str(temp_dir)) QMessageBox.information(self, "다운로드 완료", f"파일이 다운로드되었습니다.\n위치: {download_path}") self._reset_extension_button() except requests.exceptions.RequestException as e: self.logger.log(f"확장 프로그램 다운로드 실패: {e}", level=logging.ERROR, exc_info=True) QMessageBox.critical(self, "다운로드 오류", f"확장 프로그램 다운로드에 실패했습니다:\n{e}") self._reset_extension_button() except Exception as e: self.logger.log(f"확장 프로그램 설치 실패: {e}", level=logging.ERROR, exc_info=True) QMessageBox.critical(self, "오류", f"확장 프로그램 설치에 실패했습니다:\n{e}") self._reset_extension_button() def _reset_extension_button(self): """확장 설치 버튼 상태 초기화""" self.extension_install_button.setText("🧩 확장\n설치") self.extension_install_button.setEnabled(True) def _auto_set_cpu_mode(self): """CPU 모드로 모든 설정 자동 변경 (안전 모드)""" try: gpu_settings = [ 'optionIMGTrans_type', 'detail_IMGTrans_type', 'thumb_trans_type' ] for setting in gpu_settings: if hasattr(self, setting + '_combo'): combo = getattr(self, setting + '_combo') combo.setCurrentText("CPU") if hasattr(self, 'toggle_states'): self.toggle_states[setting] = "CPU" # GPU 관련 설정 비활성화 if hasattr(self, 'toggle_states'): self.toggle_states['migan_use_cuda'] = False self.toggle_states['use_cuda'] = False self.logger.log("🔒 모든 설정이 안전한 CPU 모드로 자동 변경됨", level=logging.INFO) QMessageBox.information(self, "설정 완료", "모든 번역 설정이 안전한 CPU 모드로 변경되었습니다!") except Exception as e: self.logger.log(f"CPU 모드 자동 설정 실패: {e}", level=logging.ERROR) def get_gpu_info(self): """GPU 정보를 상세히 분석하여 반환합니다.""" gpu_info = { 'has_directx12': False, 'gpu_type': 'unknown', # 'integrated', 'dedicated', 'unknown' 'vendor': 'unknown', # 'intel', 'amd', 'nvidia', 'unknown' 'model_name': '', 'recommended_model': 'simp' # 'fp16', 'opt', 'simp' } try: import wmi import platform # Windows 10 이상인지 확인 if platform.system() != "Windows": self.logger.log("Windows가 아닌 시스템에서는 DirectX 12를 지원하지 않습니다.", level=logging.DEBUG) return gpu_info windows_version = platform.version() if not windows_version.startswith("10.0"): self.logger.log(f"Windows 10 이상이 아닙니다. 현재 버전: {windows_version}", level=logging.DEBUG) return gpu_info # WMI를 통해 GPU 정보 확인 c = wmi.WMI() primary_gpu = None all_gpus = [] # 모든 GPU 정보 수집 for gpu in c.Win32_VideoController(): if gpu.Name and gpu.DriverVersion: gpu_data = { 'name': gpu.Name, 'driver_version': gpu.DriverVersion, 'adapter_ram': getattr(gpu, 'AdapterRAM', 0) or 0 } all_gpus.append(gpu_data) self.logger.log(f"GPU 발견: {gpu.Name}, RAM: {gpu_data['adapter_ram']}", level=logging.DEBUG) if not all_gpus: return gpu_info # 가장 성능이 좋을 것 같은 GPU 선택 (RAM이 많은 것 우선) primary_gpu = max(all_gpus, key=lambda x: x['adapter_ram']) gpu_name = primary_gpu['name'].lower() gpu_info['model_name'] = primary_gpu['name'] gpu_info['has_directx12'] = True # 벤더 감지 if any(vendor in gpu_name for vendor in ['intel', 'iris', 'uhd']): gpu_info['vendor'] = 'intel' elif any(vendor in gpu_name for vendor in ['amd', 'radeon']): gpu_info['vendor'] = 'amd' elif any(vendor in gpu_name for vendor in ['nvidia', 'geforce', 'quadro', 'gtx', 'rtx']): gpu_info['vendor'] = 'nvidia' # GPU 타입 감지 (내장/외장) if gpu_info['vendor'] == 'intel': # Intel은 대부분 내장 GPU gpu_info['gpu_type'] = 'integrated' gpu_info['recommended_model'] = 'opt' # 내장 GPU는 opt 모델 추천 elif gpu_info['vendor'] == 'amd': # AMD APU (내장) vs 외장 GPU 구분 if any(keyword in gpu_name for keyword in ['vega', 'renoir', 'cezanne', 'barcelo', 'apu']): gpu_info['gpu_type'] = 'integrated' gpu_info['recommended_model'] = 'opt' elif any(keyword in gpu_name for keyword in ['rx', 'radeon pro', 'r9', 'r7', 'r5']): gpu_info['gpu_type'] = 'dedicated' # 최신 RDNA 아키텍처는 fp16 추천 if any(keyword in gpu_name for keyword in ['rx 6', 'rx 7', 'rdna']): gpu_info['recommended_model'] = 'fp16' else: gpu_info['recommended_model'] = 'opt' else: gpu_info['gpu_type'] = 'unknown' gpu_info['recommended_model'] = 'opt' elif gpu_info['vendor'] == 'nvidia': # NVIDIA는 대부분 외장 GPU gpu_info['gpu_type'] = 'dedicated' # RTX 시리즈는 fp16 추천, GTX는 opt 추천 if any(keyword in gpu_name for keyword in ['rtx', 'titan']): gpu_info['recommended_model'] = 'fp16' elif any(keyword in gpu_name for keyword in ['gtx 16', 'gtx 20', 'gtx 30', 'gtx 40']): gpu_info['recommended_model'] = 'fp16' else: gpu_info['recommended_model'] = 'opt' self.logger.log(f"GPU 분석 완료: {gpu_info}", level=logging.INFO) return gpu_info except ImportError: self.logger.log("WMI 모듈을 사용할 수 없습니다. 기본 설정으로 가정합니다.", level=logging.WARNING) import platform if platform.system() == "Windows" and platform.version().startswith("10.0"): gpu_info['has_directx12'] = True gpu_info['recommended_model'] = 'opt' # 안전한 기본값 return gpu_info except Exception as e: self.logger.log(f"GPU 정보 분석 중 오류: {e}", level=logging.ERROR) return gpu_info def check_directx12_support(self): """DirectX 12 지원 여부를 확인합니다.""" gpu_info = self.get_gpu_info() return gpu_info['has_directx12'] def get_translation_engine_options(self): """ 번역 엔진 옵션 리스트를 동적으로 생성 DirectX 12 사용 불가능 시 GPU 옵션 제외 """ options = ["CPU"] # 기본 CPU 옵션 # DirectX 12 지원 가능한 경우에만 GPU 옵션 추가 gpu_available = self.check_directx12_support() self.logger.log(f"DirectX 12 지원 여부: {gpu_available}", level=logging.INFO) if gpu_available: options.append("GPU") self.logger.log("번역 엔진 옵션에 GPU 추가됨 (DirectX 12 지원)", level=logging.INFO) else: self.logger.log("번역 엔진 옵션에서 GPU 제외됨 (DirectX 12 미지원)", level=logging.INFO) # 자체서버는 항상 추가 (멤버십에 따라 사용 제한은 별도 처리) options.append("자체서버") return options def validate_gpu_option_availability(self, combo_box, setting_key): """ GPU 옵션이 선택되어 있지만 DirectX 12를 사용할 수 없는 경우 CPU로 폴백 """ current_text = combo_box.currentText() if current_text == "GPU": # DirectX 12 사용 불가능한 경우 CPU로 폴백 if not self.check_directx12_support(): self.logger.log(f"{setting_key}: GPU 선택됨 but DirectX 12 사용 불가 → CPU로 폴백", level=logging.WARNING) combo_box.setCurrentText("CPU") # toggle_states도 업데이트 if hasattr(self, 'toggle_states'): self.toggle_states[setting_key] = "CPU" # 사용자에게 알림 QMessageBox.information( self, "GPU 사용 불가", f"'{setting_key}' 설정에서 GPU가 선택되어 있었지만 DirectX 12를 사용할 수 없습니다.\n" "CPU 모드로 자동 변경되었습니다.\n\n" "GPU 사용을 원한다면 '🎮 GPU 상태' 버튼을 클릭하여 설정을 확인해주세요." ) def validate_all_translation_engines(self): """ 번역 엔진 설정이 ImageProcessorDialog로 통합됨 더 이상 개별 콤보박스 검증 불필요 """ # 번역 엔진은 ImageProcessorDialog에서 통합 관리 pass # def on_manual_button_clicked(self): # """매뉴얼 버튼 클릭 시 호출""" # self.logger.log("매뉴얼 버튼 클릭됨", level=logging.DEBUG) # user_manual_widget = UserManualDialog(self) # user_manual_widget.exec() 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 on_worker_status_message(self, message: str, status: str): """워커 상태 메시지를 상태바에 표시""" try: # 상태바에 메시지 표시 if hasattr(self, 'statusbar'): # 상태에 따라 표시 시간 조정 timeout = 5000 if status == "success" else 8000 if status == "error" else 3000 self.statusbar.showMessage(message, timeout) # 로그에도 기록 log_level = { "info": logging.INFO, "success": logging.INFO, "warning": logging.WARNING, "error": logging.ERROR }.get(status, logging.INFO) if hasattr(self, 'logger') and self.logger: self.logger.log(f"[워커 상태] {message}", level=log_level) except Exception as e: if hasattr(self, 'logger') and self.logger: self.logger.log(f"워커 상태 메시지 표시 중 오류: {e}", level=logging.ERROR) def on_worker_health_status(self, is_healthy: bool): """워커 헬스 상태에 따라 편집알바생 버튼 활성화/비활성화""" try: if hasattr(self, 'start_chrome_button'): self.start_chrome_button.setEnabled(is_healthy) if not is_healthy: # 워커 비정상 - 편집알바생 모드 비활성화 메시지 if hasattr(self, 'logger') and self.logger: self.logger.log( "이미지 워커 비정상 상태로 인해 편집알바생 모드가 비활성화되었습니다. " "업로드알바생 모드는 정상적으로 사용 가능합니다.", level=logging.WARNING ) # 사용자에게 안내 메시지 표시 QMessageBox.warning( self, "⚠️ 이미지 워커 비정상", "이미지 워커가 정상적으로 작동하지 않아\n" "편집알바생 모드를 사용할 수 없습니다.\n\n" "📌 현재 상황:\n" " • 편집알바생 모드: ❌ 사용 불가\n" " • 업로드알바생 모드: ✅ 사용 가능\n\n" "💡 해결 방법:\n" " 1. [이미지 프로세서 관리] 메뉴에서 워커 상태를 확인하세요\n" " 2. 프로세서 재시작을 시도해보세요\n" " 3. 문제가 지속되면 프로세서를 재설치하세요" ) else: # 워커 정상 - 편집알바생 모드 활성화 if hasattr(self, 'logger') and self.logger: self.logger.log( "이미지 워커가 정상 작동하여 편집알바생 모드가 활성화되었습니다.", level=logging.INFO ) # 상태바에 긍정적인 메시지 표시 if hasattr(self, 'statusbar'): self.statusbar.showMessage("✅ 모든 기능이 정상적으로 사용 가능합니다", 3000) except Exception as e: if hasattr(self, 'logger') and self.logger: self.logger.log(f"워커 헬스 상태 처리 중 오류: {e}", level=logging.ERROR, exc_info=True) def showEvent(self, event): """창 표시 시 이미지 프로세서 자동 초기화""" super().showEvent(event) # 한 번만 자동 초기화 수행 if not self.image_processor_auto_initialized: self.image_processor_auto_initialized = True try: self.logger.log("MAIN_GUI 표시 시 이미지 프로세서 자동 초기화 시작", level=logging.INFO) # 다이얼로그의 initialize_processor 호출 (백그라운드에서 자동 처리) self.image_processor_dialog.initialize_processor() except Exception as e: self.logger.log(f"이미지 프로세서 자동 초기화 실패: {e}", level=logging.ERROR, exc_info=True) 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.DEBUG) 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.DEBUG) else: self.logger.log(f"종료 시 카테고리 데이터 Supabase 동기화 실패", level=logging.WARNING) # Playwright 및 이벤트 루프 정리 # asyncio.run(self.cleanup_resources()) # 브라우저 컨트롤러 스레드 종료 if self.browser_controller.isRunning(): self.browser_controller.terminate() self.browser_controller.wait(3000) if self.browser_controller.isRunning(): self.logger.log('스레드가 종료되지 않아 강제 종료를 시도합니다.', level=logging.WARNING) self.browser_controller.terminate() # 강제 종료 # 이미지 프로세서 정리 if hasattr(self, 'image_processor_manager') and self.image_processor_manager: try: self.logger.log("이미지 프로세서를 정리합니다.", level=logging.INFO) self.image_processor_manager.cleanup() self.logger.log("이미지 프로세서 정리 완료", level=logging.INFO) except Exception as e: self.logger.log(f"이미지 프로세서 정리 중 오류: {e}", level=logging.ERROR) # 세션 종료 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) # is_valid_level = self.user_membership_level == 'premium' or self.user_membership_level == 'vip' # is_level_vip = self.user_membership_level == 'vip' # self.logger.log(f"is_valid_level: {is_valid_level}", level=logging.DEBUG) is_trans_enabled = self.toggle_states.get('optionIMGTrans', False) or self.toggle_states.get('detail_IMGTrans', False) or self.toggle_states.get('thumb', False) or self.toggle_states.get('thumb_nukki', False) self.browser_controller.is_trans_enabled = is_trans_enabled self.browser_controller.authenticated_by_admin = self.authenticated_by_admin self.browser_controller.user_membership_level = self.user_membership_level self.logger.log(f"user_membership_level: {self.user_membership_level}", level=logging.DEBUG) self.logger.log(f"authenticated_by_admin: {self.authenticated_by_admin}", level=logging.DEBUG) self.logger.log(f"is_trans_enabled: {is_trans_enabled}", level=logging.DEBUG) self.browser_controller.update_image_processor_info(is_trans_enabled, self.authenticated_by_admin, self.user_membership_level) # try: # if is_valid_level and is_trans_enabled: # from src.modules.image_processor3 import ImageProcessor3 # self.image_processor = ImageProcessor3(self.logger, self.browser_controller.page, self.toggle_states, self.toggle_states['unwanted_words'], self.base_dir) # self.browser_controller.image_processor = self.image_processor # except Exception as e: # self.logger.log(f"ImageProcessor3 초기화 중 오류 발생: {e}", level=logging.ERROR, exc_info=True) # 번역 엔진 설정을 ImageProcessorDialog의 설정으로 통합 translation_engine = self.settings_manager.get_value("image_processor/translation_engine", "CPU") self.toggle_states['optionIMGTrans_type'] = translation_engine self.toggle_states['thumb_trans_type'] = translation_engine self.toggle_states['detail_IMGTrans_type'] = translation_engine # inpaint_method 업데이트 (번역 엔진 설정에 따라 연동) method_map = { "CPU": "CPU", "GPU": "GPU", "자체서버": "external_request" } inpaint_method = method_map.get(translation_engine, "CPU") self.toggle_states['inpaint_method'] = inpaint_method self.logger.log(f"번역 엔진 설정 적용: {translation_engine}, inpaint_method: {inpaint_method}", level=logging.INFO) self.browser_controller.update_toggle_states(self.toggle_states) 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(), 'user_id': self.user_id_input.text(), 'user_pw': self.user_pw_input.text(), 'is_admin': self.admin_toggle.isChecked(), } # 로그인 정보 저장 self.save_settings() # 스레드 시작 self.browser_controller.start_browser_task() else: self.logger.log("브라우저 스레드가 실행중이지 않습니다.", level=logging.WARNING) self.toggle_settings_button.setEnabled(True) @Slot() def on_browser_started(self): """브라우저 시작 완료 시 처리할 로직""" self.logger.log("브라우저가 성공적으로 시작되었습니다.", level=logging.INFO) # 그룹 Refresh 버튼 활성화 if hasattr(self, 'group_refresh_button') and self.group_refresh_button is not None: self.group_refresh_button.setEnabled(True) self.logger.log("그룹 새로고침 버튼이 활성화되었습니다.", level=logging.INFO) # VIP 사용자의 경우 업로드정보삭제 버튼 활성화 try: membership = (self.user_membership_level or 'free').lower() is_vip_or_admin = membership in ['vip', 'admin'] if is_vip_or_admin and hasattr(self, 'market_change_button') and self.market_change_button is not None: self.market_change_button.setEnabled(True) self.logger.log("마켓API변경 버튼이 활성화되었습니다.", level=logging.INFO) except Exception as e: self.logger.log(f"마켓API변경 버튼 활성화 실패: {e}", level=logging.ERROR) # 버튼 상태 활성화&비활성화 # 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_image_processor_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_image_worker_fatal(self, error_message): """ImageWorker 오류 발생 시 처리할 로직""" self.logger.log(f"ImageWorker 오류 발생: {error_message}", level=logging.ERROR, exc_info=True) QMessageBox.critical(self, "오류", f"ImageWorker 오류 발생: {error_message}") sys.exit(1) @Slot(str) def on_premium_event_started(self, error_message): """프리미엄 이벤트 시작 시 처리할 로직""" self.logger.log(f"프리미엄 이벤트 시작: {error_message}", level=logging.INFO) QMessageBox.information(self, "정보", f"프리미엄 이벤트 시작: {error_message}") @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_complete_remove_market_info_job(self, result_message): """업로드정보삭제 완료 시 처리할 로직""" self.logger.log(f"업로드정보삭제 완료: {result_message}", level=logging.INFO) # 다중 작업(순차 업로드) 진행 중인지 확인 is_multi_job = hasattr(self, 'current_upload_state') and self.current_upload_state is not None if is_multi_job: self.show_auto_close_message("완료", "업로드정보가 삭제되었습니다.\n\n(2초 후 자동으로 닫힙니다)", 2000) else: QMessageBox.information(self, "완료", f"업로드정보가 삭제되었습니다.") # @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) 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): """ 활성 세션 목록을 참고해, 작업 중인 그룹이면 (작업중) 표시를 붙이고 선택도 비활성화. """ # ed_mode(등록모드)가 켜져 있으면 활성 세션 검사를 건너뛴다 try: if getattr(self, 'toggle_states', {}).get('ed_mode', False): self.group_selector.blockSignals(True) self.group_selector.clear() for group in group_names_list: self.group_selector.addItem(group) self.group_selector.blockSignals(False) self.group_selector.setEnabled(True) if hasattr(self, 'group_change_button') and self.group_change_button is not None: self.group_change_button.setEnabled(True) # 그룹 Refresh 버튼 활성화 (브라우저가 실행 중인 경우) if hasattr(self, 'group_refresh_button') and self.group_refresh_button is not None: if hasattr(self, 'browser_controller') and self.browser_controller.isRunning(): self.group_refresh_button.setEnabled(True) return except Exception: pass # 시그널 일시 차단 → ComboBox 업데이트 중 의도하지 않은 선택 변경 방지 self.group_selector.blockSignals(True) 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.blockSignals(False) self.group_selector.setEnabled(any_group_available) self.group_change_button.setEnabled(any_group_available) # 그룹 Refresh 버튼 다시 활성화 (브라우저가 실행 중인 경우) if hasattr(self, 'group_refresh_button') and self.group_refresh_button is not None: if hasattr(self, 'browser_controller') and self.browser_controller.isRunning(): self.group_refresh_button.setEnabled(True) if not any_group_available: QMessageBox.critical( self, "오류", "선택 가능한 그룹이 없습니다. 모두 작업 중입니다.\n다른 PC에서 그룹 작업이 끝날 때까지 기다려주세요.", ) sys.exit(1) self.restore_selected_group() def restore_selected_group(self): """ QLabel(self.selected_group)의 텍스트와 일치하는 그룹을 group_selector에서 찾아 선택한다. 단, '(작업중)' 상태인 그룹은 선택하지 않는다. """ try: selected_group_text = self.selected_group.text().strip() if selected_group_text == "": return # 선택할 그룹 없음 for i in range(self.group_selector.count()): item_text = self.group_selector.itemText(i) # '(작업중)' 표시가 있는 경우는 선택하지 않음 if "(작업중)" in item_text: continue base_text = item_text.replace(" (작업중)", "") if base_text == selected_group_text: self.group_selector.setCurrentIndex(i) break except Exception as e: self.logger.log(f"restore_selected_group 실행 중 오류: {e}", level=logging.ERROR, exc_info=True) @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) # VIP이면 업로드정보삭제 버튼 활성화, 등록모드+VIP이면 마켓정보변경 버튼도 활성화 try: is_ed_mode = self.toggle_states.get('ed_mode', False) membership = (self.user_membership_level or 'free').lower() is_vip_or_admin = membership in ['vip', 'admin'] if hasattr(self, 'remove_upload_info_button') and self.remove_upload_info_button is not None: self.remove_upload_info_button.setEnabled(is_vip_or_admin) # 알바생 로그인 완료 시 VIP면 활성화 if hasattr(self, 'market_change_button') and self.market_change_button is not None: self.market_change_button.setEnabled(is_ed_mode and is_vip_or_admin) if hasattr(self, 'upload_selected_markets_button') and self.upload_selected_markets_button is not None: self.upload_selected_markets_button.setEnabled(is_vip_or_admin) if hasattr(self, 'upload_selected_markets_Multi_button') and self.upload_selected_markets_Multi_button is not None: self.upload_selected_markets_Multi_button.setEnabled(is_vip_or_admin) except Exception: pass self.selected_group_total_products.setText(f"총 {total_products}개 상품") self.total_product_count = total_products @Slot() def on_start_PercentyJob_clicked(self): # 이미지 프로세서 실행 확인 및 자동 실행 try: self.logger.log("이미지 프로세서 실행 상태 확인 중...", level=logging.INFO) # 이미지 프로세서가 실행되고 있는지 확인 if not self.image_processor_manager.is_running(): self.logger.log("이미지 프로세서가 실행되지 않고 있습니다. 자동 실행을 시도합니다.", level=logging.WARNING) # 설치 확인 if self.image_processor_manager.status == self.image_processor_manager.status.NOT_INSTALLED: QMessageBox.warning( self, "이미지 프로세서 미설치", "이미지 프로세서가 설치되어 있지 않습니다.\n\n" "설정 버튼의 '🖼️ 이미지 프로세서' 버튼을 클릭하여\n" "먼저 설치 및 설정을 완료해주세요." ) return # 프로세서 시작 시도 if not self.image_processor_manager.start_processor(): QMessageBox.warning( self, "이미지 프로세서 실행 실패", "이미지 프로세서를 시작할 수 없습니다.\n\n" "설정 버튼의 '🖼️ 이미지 프로세서' 버튼을 클릭하여\n" "상태를 확인하고 문제를 해결해주세요." ) return # 시작 후 잠시 대기하여 안정화 import time time.sleep(2) self.logger.log("이미지 프로세서 자동 실행 성공", level=logging.INFO) else: self.logger.log("이미지 프로세서가 이미 실행 중입니다.", level=logging.INFO) except Exception as e: self.logger.log(f"이미지 프로세서 확인 중 오류: {e}", level=logging.ERROR, exc_info=True) QMessageBox.warning( self, "오류", f"이미지 프로세서 상태 확인 중 오류가 발생했습니다:\n{e}\n\n" "작업을 계속 진행하시겠습니까?" ) # 로그인 후 설정 버튼 비활성화 try: self.toggle_settings_button.setEnabled(False) self.setting_Buttons_group.setVisible(False) self.job_group_widget.setVisible(False) except Exception as e: self.logger.log(f"토글설정 버튼 비활성화 중 오류: {e}", level=logging.ERROR, exc_info=True) if self.selected_group_total_products.text() == "총 0개 상품": QMessageBox.warning(self, "오류", "상품이 없습니다. 다른 그룹을 선택하거나 해당그룹에 상품을 추가해주세요.") return QMessageBox.information(self, "정보", "알바생 실행중 자동으로 생성되는 웨일 / 크롬등의 브라우저 창은 종료하지 말아주세요.") # 번역 엔진 설정을 ImageProcessorDialog의 설정으로 통합 translation_engine = self.settings_manager.get_value("image_processor/translation_engine", "CPU") self.toggle_states['optionIMGTrans_type'] = translation_engine self.toggle_states['thumb_trans_type'] = translation_engine self.toggle_states['detail_IMGTrans_type'] = translation_engine self.logger.log(f"번역 엔진 설정 적용: {translation_engine}", level=logging.INFO) self.browser_controller.update_toggle_states(self.toggle_states) self.logger.log(f"start_PercentyJob 토글상태 재전송[self.toggle_states]", level=logging.DEBUG) self.logger.log(f"widget_map 전송[self.widget_map]", level=logging.DEBUG) # ed_mode & VIP/Admin인 경우 전체 사업자 정보 전달 try: is_ed_mode = self.toggle_states.get('ed_mode', False) membership = (self.user_membership_level or 'free').lower() is_vip_or_admin = membership in ['vip', 'admin'] if is_ed_mode and is_vip_or_admin and hasattr(self, 'biz_dbManager') and self.biz_dbManager: # 선택마켓 기준 정보 전달 legacy_info = self.biz_dbManager.get_biz_full_info_legacy() self.browser_controller.update_biz_info(legacy_info) self.logger.log(f"선택마켓 정보 전송 완료 - {len(legacy_info.get('markets', {}))}개 마켓", level=logging.DEBUG) except Exception as e: self.logger.log(f"biz_info 전달 중 오류: {e}", level=logging.ERROR, exc_info=True) if self.selected_group.text() == "없음": QMessageBox.warning(self, "오류", "그룹을 선택해주세요.") return if not self.toggle_states.get('ed_mode', False): 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.DEBUG) self.logger.log(f"conflicting_sessions: {conflicting_sessions}", level=logging.DEBUG) self.logger.log(f"message: {message}", level=logging.DEBUG) if has_conflict: QMessageBox.warning(self, "오류", "선택한 그룹이 작업중입니다. 다른 그룹을 선택하세요.") return # ed_mode면 그룹명에 [등록모드] 태그 추가하여 세션에 기록 try: group_name_to_store = self.selected_group.text() if self.toggle_states.get('ed_mode', False) and "[등록모드]" not in group_name_to_store: group_name_to_store = f"{group_name_to_store} [등록모드]" except Exception: group_name_to_store = self.selected_group.text() self.supabase_manager.update_session_selected_group(selected_group=group_name_to_store) 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) # 프로그래스바 초기화 try: self.logger.log("프로그래스바 초기화 시작", level=logging.DEBUG) self.update_total_progress(0,0) self.logger.log("프로그래스바 초기화 완료", level=logging.INFO) except Exception as e: self.logger.log(f"❌ 프로그래스바 초기화 실패: {e}", level=logging.ERROR, exc_info=True) QMessageBox.critical(self, "오류", f"프로그래스바 초기화 중 오류가 발생했습니다:\n{e}") return # 토글 상태에 따라 활성화된 스테이지 목록 생성 try: self.logger.log("활성 스테이지 목록 생성 시작", level=logging.DEBUG) active_stages = [] # 상품명 스테이지 체크 (안전하게) if (hasattr(self, 'title_toggle') and self.title_toggle.isChecked()) or \ (hasattr(self, 'title_shuffle_toggle') and self.title_shuffle_toggle.isChecked()): active_stages.append("상품명") self.logger.log("상품명 스테이지 추가", level=logging.DEBUG) # 옵션 스테이지 체크 (안전하게) if (hasattr(self, 'optionTrnas_toggle') and self.optionTrnas_toggle.isChecked()) or \ (hasattr(self, 'optionAutoSelect_toggle') and self.optionAutoSelect_toggle.isChecked()) or \ (hasattr(self, 'optionIMGTrans_toggle') and self.optionIMGTrans_toggle.isChecked()): active_stages.append("옵션") self.logger.log("옵션 스테이지 추가", level=logging.DEBUG) # 가격 스테이지 체크 (안전하게) if (hasattr(self, 'price_toggle') and self.price_toggle.isChecked()) or \ (hasattr(self, 'remove_overprice_toggle') and self.remove_overprice_toggle.isChecked()): active_stages.append("가격") self.logger.log("가격 스테이지 추가", level=logging.DEBUG) # 썸네일 스테이지 체크 (안전하게) if (hasattr(self, 'thumb_toggle') and self.thumb_toggle.isChecked()) or \ (hasattr(self, 'thumb_nukki_toggle') and self.thumb_nukki_toggle.isChecked()): active_stages.append("썸네일") self.logger.log("썸네일 스테이지 추가", level=logging.DEBUG) # 태그 스테이지 체크 (안전하게) # 참고: tag_ai는 toggle_states에만 존재하며, 별도 위젯은 없음 if hasattr(self, 'tag_toggle') and self.tag_toggle.isChecked(): active_stages.append("태그") self.logger.log("태그 스테이지 추가", level=logging.DEBUG) # 상페 스테이지 체크 (안전하게) if (hasattr(self, 'detail_Option_toggle') and self.detail_Option_toggle.isChecked()) or \ (hasattr(self, 'detail_IMGTrans_toggle') and self.detail_IMGTrans_toggle.isChecked()): active_stages.append("상페") self.logger.log("상페 스테이지 추가", level=logging.DEBUG) self.logger.log(f"✅ 활성 스테이지 목록 생성 완료: {active_stages}", level=logging.INFO) except Exception as e: self.logger.log(f"❌ 스테이지 목록 생성 실패: {e}", level=logging.ERROR, exc_info=True) QMessageBox.critical(self, "오류", f"스테이지 목록 생성 중 오류가 발생했습니다:\n{e}") return # 스테이지 타임라인 업데이트 try: self.logger.log("스테이지 타임라인 업데이트 시작", level=logging.DEBUG) self.update_stage_timeline(active_stages) self.logger.log("✅ 스테이지 타임라인 업데이트 완료", level=logging.INFO) except Exception as e: self.logger.log(f"❌ 스테이지 타임라인 업데이트 실패: {e}", level=logging.ERROR, exc_info=True) QMessageBox.critical(self, "오류", f"스테이지 타임라인 업데이트 중 오류가 발생했습니다:\n{e}") return """상품수정 스레드 시작 및 상태 전달""" try: self.logger.log("브라우저 컨트롤러 상태 확인 시작", level=logging.DEBUG) if self.browser_controller.isRunning(): self.logger.log("✅ 브라우저 스레드가 실행 중입니다.", level=logging.DEBUG) # 스레드 시작 self.logger.log("상품수정 작업 시작 요청", level=logging.DEBUG) self.browser_controller.start_PercentyJob_task() self.logger.log("✅ 상품수정 작업 스레드가 시작되었습니다.", level=logging.INFO) else: self.logger.log("❌ 브라우저 스레드가 실행되지 않았습니다.", level=logging.WARNING) QMessageBox.warning(self, "경고", "브라우저가 실행되지 않았습니다.\n브라우저를 먼저 시작해주세요.") except Exception as e: self.logger.log(f"❌ 작업 시작 중 오류 발생: {e}", level=logging.ERROR, exc_info=True) QMessageBox.critical(self, "오류", f"작업 시작 중 오류가 발생했습니다:\n{e}") @Slot() def on_session_cleanup_button_clicked(self): """세션 정리 버튼 클릭 핸들러""" try: self.logger.log("세션 정리 시작", level=logging.INFO) # 세션 정리 실행 result = self.supabase_manager.cleanup_inactive_sessions(self.sp_user_id, inactive_minutes=1) if result.get("success"): cleaned_count = result.get("cleaned_count", 0) message = result.get("message", "") # 메시지 박스 표시 QMessageBox.information( self, "세션 정리 완료", message ) self.logger.log(f"세션 정리 완료: {cleaned_count}개 세션 정리됨", level=logging.INFO) # 세션 정리 후 그룹 목록 새로고침 (브라우저가 실행 중인 경우에만) if hasattr(self, 'browser_controller') and self.browser_controller.isRunning(): self.logger.log("세션 정리 후 그룹 목록 새로고침 시작 (2초 대기 후)", level=logging.INFO) # 데이터베이스 업데이트가 반영될 때까지 잠시 대기 # 세션 정리 후 활성 세션 조회가 최신 상태를 반영하도록 지연 def refresh_group_list(): self.logger.log("세션 정리 후 그룹 목록 새로고침 실행", level=logging.INFO) self.browser_controller.get_group_names_list_all_task() QTimer.singleShot(2000, refresh_group_list) else: QMessageBox.warning( self, "알림", "브라우저가 실행 중이지 않아 그룹 목록을 새로고침할 수 없습니다.\n브라우저를 시작한 후 '그룹 새로고침' 버튼을 눌러주세요." ) else: error_message = result.get("message", "알 수 없는 오류") QMessageBox.warning( self, "세션 정리 실패", f"세션 정리 중 오류가 발생했습니다:\n{error_message}" ) self.logger.log(f"세션 정리 실패: {error_message}", level=logging.ERROR) except Exception as e: self.logger.log(f"세션 정리 버튼 클릭 중 오류: {e}", level=logging.ERROR, exc_info=True) QMessageBox.critical( self, "오류", f"세션 정리 중 예상치 못한 오류가 발생했습니다:\n{str(e)}" ) @Slot() def on_group_refresh_button_clicked(self): """그룹 새로고침 버튼 클릭 핸들러""" try: if not hasattr(self, 'browser_controller') or not self.browser_controller.isRunning(): QMessageBox.warning( self, "알림", "브라우저가 실행 중이지 않습니다.\n먼저 '편집알바생 로그인' 버튼을 눌러 브라우저를 시작해주세요." ) return self.logger.log("그룹 목록 새로고침 시작", level=logging.INFO) self.group_refresh_button.setEnabled(False) # 중복 클릭 방지 # 그룹 목록 가져오기 self.browser_controller.get_group_names_list_all_task() except Exception as e: self.logger.log(f"그룹 새로고침 버튼 클릭 중 오류: {e}", level=logging.ERROR, exc_info=True) QMessageBox.critical( self, "오류", f"그룹 목록 새로고침 중 예상치 못한 오류가 발생했습니다:\n{str(e)}" ) if hasattr(self, 'group_refresh_button'): self.group_refresh_button.setEnabled(True) 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.DEBUG) # self.group_change_button.setEnabled(False) if self.browser_controller.isRunning(): selected_text = self.group_selector.currentText() self.logger.log(f"{selected_text} 그룹 선택", level=logging.DEBUG) self.browser_controller.select_group_task(group_name=selected_text) # ✅ 인덱스 대신 텍스트 전달 self.logger.log("그룹선택 작업이 QThread에서 시작되었습니다.", level=logging.DEBUG) 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.DEBUG) # 디스코드 알림 전송 (토글이 활성화된 경우에만) 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 ) try: self.PercentyJob_button.setEnabled(True) self.setting_Buttons_group.setVisible(True) self.job_group_widget.setVisible(True) except Exception as e: self.logger.log(f"상품수정 완료 시 버튼 활성화 중 오류: {e}", level=logging.ERROR, exc_info=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: # 남은 기간이 7일 이내인 경우 가격표 다이얼로그 표시 warning_membership_msg = self.supabase_manager.get_membership_message(self.user_info) if warning_membership_msg: self.show_pricing_dialog() def show_pricing_dialog(self): """가격표 다이얼로그를 표시합니다. Supabase의 events 테이블에서 가격표 HTML을 가져와 표시합니다. """ try: dialog = QDialog(self) dialog.setWindowTitle("가격표") dialog.setModal(True) dialog.resize(600, 700) layout = QVBoxLayout() # HTML 텍스트 브라우저 생성 from PySide6.QtWidgets import QTextBrowser text_browser = QTextBrowser() # Supabase에서 가격표 HTML 가져오기 pricing_html = self.supabase_manager.get_pricing_html() text_browser.setHtml(pricing_html) layout.addWidget(text_browser) # 닫기 버튼 close_button = QPushButton("닫기") close_button.clicked.connect(dialog.accept) layout.addWidget(close_button) dialog.setLayout(layout) dialog.exec_() except Exception as e: self.logger.error(f"가격표 다이얼로그 표시 중 오류 발생: {e}") QMessageBox.warning(self, "오류", f"가격표를 표시하는 중 오류가 발생했습니다: {e}") def show_membership_warning(self, warning_membership_msg): """HTML을 지원하는 멤버십 경고 메시지 표시""" msg_box = QMessageBox(self) msg_box.setWindowTitle("재결제 안내") msg_box.setTextFormat(Qt.RichText) # HTML 형식 지원 msg_box.setText(warning_membership_msg) msg_box.setIcon(QMessageBox.Information) msg_box.exec_() def is_premium_or_higher(self): """사용자 등급이 Premium 이상인지 확인""" membership_level = self.user_info.get('membership_level', 'free') self.logger.log(f"프리미엄 등급 확인 - 현재 등급: {membership_level}", level=logging.DEBUG) return membership_level in ['premium', 'vip', 'admin'] def is_vip_user(self): """사용자 등급이 VIP인지 확인""" membership_level = self.user_info.get('membership_level', 'free') self.logger.log(f"VIP 체크 - 사용자 등급: {membership_level}", level=logging.DEBUG) return membership_level in ['vip', 'admin'] def check_vip_feature_access(self, feature_name="VIP 전용 기능"): """VIP 전용 기능 접근 권한을 체크하고, VIP가 아니면 경고 메시지 표시""" if not self.is_vip_user(): msg_box = QMessageBox(self) msg_box.setWindowTitle("VIP 전용 기능") msg_box.setTextFormat(Qt.RichText) # HTML 형식 지원 msg_box.setText(f"""

🔒 VIP 전용 기능

{feature_name}은(는) VIP 멤버십에서만 사용할 수 있습니다.

VIP 멤버십으로 업그레이드하시면:

VIP 멤버십에 대한 자세한 정보는 고객센터에 문의해주세요.

""") msg_box.setIcon(QMessageBox.Warning) msg_box.exec_() return False return True def on_first_option_img_to_thumb_toggle_clicked(self, checked): """첫 번째 옵션 썸네일 복사 토글 클릭 핸들러 (VIP 전용 기능)""" if checked: # 토글이 ON으로 변경될 때만 VIP 체크 if not self.check_vip_feature_access("첫 번째 옵션 썸네일 복사"): # VIP가 아니면 토글을 강제로 OFF로 설정 self.first_option_img_to_thumb_toggle.setChecked(False) return # VIP 사용자이거나 토글이 OFF로 변경되는 경우 정상 처리 self.universal_input_handler(self.first_option_img_to_thumb_toggle, checked) def on_unwanted_words_button_clicked(self): """불필요한 단어 설정 다이얼로그 표시""" try: # 현재 unwanted_words 가져오기 current_words = self.toggle_states.get('unwanted_words', {}) # 데이터가 비어있거나 기존 형태인 경우 기본값 설정 if not current_words: current_words = self.set_default_unwanted_words() elif isinstance(current_words, dict) and 'combined' in current_words: # 기존 형태 데이터를 새로운 형태로 변환 current_words = self.convert_old_format_to_new(current_words) self.logger.log(f"current_words: {current_words}", level=logging.DEBUG) # 다이얼로그 생성 및 표시 unwanted_dialog = UnwantedWordsDialog(parent=self, logger=self.logger, current_words=current_words) result = unwanted_dialog.exec_() # 다이얼로그가 Accepted로 종료된 경우 (확인 버튼 또는 저장 후 종료) if result == QDialog.Accepted: # 새로운 단어 리스트 가져오기 - 이미 딕셔너리 형태로 반환됨 unwanted_words = unwanted_dialog.get_words() # 단어 리스트 저장 self.toggle_states['unwanted_words'] = unwanted_words # Supabase에 동기화 self.sync_unwanted_words_to_supabase() self.logger.log(f"불필요한 단어 리스트가 업데이트되었습니다: {unwanted_words}", 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 set_default_unwanted_words(self): """불필요한 단어 리스트 기본값 설정""" # 기본값 설정 - 새로운 딕셔너리 형태 default_words = { "할인": "", "무료": "", "증정": "", "이벤트": "", "세일": "", "사은품": "", "보증": "", "품절": "", "행사": "", "할인가": "", "무료배송": "", } # toggle_states에 설정 self.toggle_states['unwanted_words'] = default_words self.logger.log("불필요한 단어 리스트 기본값이 설정되었습니다.", level=logging.INFO) return default_words def convert_old_format_to_new(self, old_data): """기존 형태 데이터를 새로운 형태로 변환""" new_data = {} if 'combined' in old_data: for item in old_data['combined']: if '(' in item and ')' in item: # "한글(중국어)" 형태를 "한글: 빈값"로 변환 korean = item.split('(')[0].strip() chinese = "" new_data[korean] = chinese else: # 단순 문자열인 경우 그대로 사용 new_data[item] = item return new_data 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 = {} # unwanted_words가 없거나 빈 딕셔너리인 경우 기본값 설정 if not unwanted_words: 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"불필요한 단어 리스트가 동기화되었습니다: {len(unwanted_words)}개 항목", 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.DEBUG) # 데이터가 있는 경우 if json_data: try: # JSON 문자열 파싱 unwanted_words = json.loads(json_data) # 기존 형태인지 확인하고 변환 if isinstance(unwanted_words, dict) and 'combined' in unwanted_words: # 기존 형태를 새로운 형태로 변환 unwanted_words = self.convert_old_format_to_new(unwanted_words) elif not isinstance(unwanted_words, dict): # 딕셔너리가 아닌 경우 기본값 설정 unwanted_words = self.set_default_unwanted_words() self.toggle_states['unwanted_words'] = unwanted_words self.logger.log(f"불필요한 단어 리스트가 로드되었습니다: {len(unwanted_words)}개 항목", 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) # 계정 메뉴 추가 account_menu = menubar.addMenu("계정") # 비밀번호 변경 password_change_action = account_menu.addAction("비밀번호 변경") password_change_action.triggered.connect(self.handle_password_change) # 도움말 메뉴 help_menu = menubar.addMenu("도움말") # 릴리즈 노트 메뉴 항목 release_note_action = help_menu.addAction("릴리즈 노트") release_note_action.triggered.connect(self.show_release_notes) # 가격표 메뉴 추가 pricing_action = help_menu.addAction("가격표") pricing_action.triggered.connect(self.show_pricing_dialog) # # 매뉴얼 # manual_action = help_menu.addAction("사용 설명서") # manual_action.triggered.connect(self.on_manual_button_clicked) # 금지어 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) # 설정 동기화 테스트 settings_sync_test_action = help_menu.addAction("설정 동기화 테스트") settings_sync_test_action.triggered.connect(self.test_settings_sync) def show_release_notes(self): """전체 릴리즈 노트 히스토리를 표시합니다.""" try: # version_manager 인스턴스 가져오기 (browser_controller에서 가져올 수 있음) from updateManager.__version__ import __version__ from updateManager.version_manager import VersionManager # version_manager가 없으면 생성 if not hasattr(self, 'version_manager'): self.version_manager = VersionManager( logger=self.logger, supabase_manager=self.supabase_manager, current_version=__version__ ) # 업데이트 히스토리 가져오기 history = self.version_manager.get_update_history() if not history: QMessageBox.information(self, "알림", "릴리즈 노트 히스토리가 없습니다.") return # 가장 최신 버전의 릴리즈 노트를 표시 latest_update = history[0] self.version_manager.show_release_note(latest_update, self) except Exception as e: self.logger.log(f"릴리즈 노트 표시 중 오류 발생: {str(e)}", level=logging.ERROR, exc_info=True) QMessageBox.warning(self, "오류", f"릴리즈 노트를 표시할 수 없습니다.\n{str(e)}") def on_debug_toggle_clicked(self, is_checked): """디버그 모드 토글 클릭 시 처리""" # 상태 업데이트 self.toggle_states['debug_mode'] = is_checked # 로그 기록 self.logger.log(f"디버그 모드 설정: {'활성화' if is_checked else '비활성화'}", level=logging.DEBUG) # # 설정 저장 # self.save_toggle_settings() @Slot(bool) def on_ocr_toggle_clicked(self, is_checked): """OCR 토글 클릭 시 처리""" # Premium 이상 권한 확인 if is_checked and not self.is_premium_or_higher(): self.ocr_toggle.setChecked(False) self.unwanted_words_button.setEnabled(False) QMessageBox.warning(self, "권한 부족", "이미지 글자 인식 기능은 Premium 이상 등급에서만 사용 가능합니다.") return # user_cpu_flags = self.avx_ckeck() # is_avx_supported = 'avx' in user_cpu_flags # if is_avx_supported: # self.avx_supported = True # else: # self.avx_supported = False # self.ocr_processor = None # self.ocr_toggle.setChecked(False) # self.unwanted_words_button.setEnabled(False) # QMessageBox.warning(self, "AVX 미지원", f"AVX 미지원 CPU 입니다. OCR 기능을 사용할 수 없습니다.\n 사용자 CPU 정보: {user_cpu_flags}") # return # # 상태 업데이트 # self.toggle_states['ocr'] = is_checked # # OCR 설정 버튼 활성화/비활성화 # self.unwanted_words_button.setEnabled(is_checked) # # 로그 기록 # self.logger.log(f"OCR 토글 상태 변경: {'활성화' if is_checked else '비활성화'}", level=logging.DEBUG) # # 설정 저장 # self.save_settings() def handle_password_change(self): """비밀번호 변경 메뉴 클릭 시 비밀번호 변경 다이얼로그를 실행합니다.""" # 로그인 상태 확인 if not self.supabase_manager or not self.supabase_manager.access_token: QMessageBox.warning( self, "로그인 필요", "비밀번호 변경을 위해서는 먼저 로그인이 필요합니다.\n\n" "프로그램을 재시작하여 로그인해 주세요." ) return # 인터넷 연결 확인 if not self.check_internet_connection(): self.logger.log("인터넷 연결 없음 - 비밀번호 변경 시도 중단", level=logging.WARNING) if not self.show_internet_connection_error(): return # 다시 시도 후에도 연결 안됨 if not self.check_internet_connection(): return try: # 비밀번호 변경 다이얼로그 실행 password_change_dialog = PasswordChangeDialog( self.logger, self.supabase_manager, self ) result = password_change_dialog.exec() if result == QDialog.Accepted: # 비밀번호 변경 성공 self.logger.log("비밀번호 변경 완료", level=logging.INFO) # 성공 메시지 QMessageBox.information( self, "비밀번호 변경 완료", "비밀번호가 성공적으로 변경되었습니다.\n\n" "보안을 위해 다른 기기에서도 새로운 비밀번호로 로그인해 주세요." ) else: # 비밀번호 변경 취소 self.logger.log("비밀번호 변경 취소", level=logging.INFO) except Exception as e: error_message = f"비밀번호 변경 중 오류가 발생했습니다: {str(e)}" self.logger.log(error_message, level=logging.ERROR, exc_info=True) QMessageBox.critical(self, "오류", error_message) def check_internet_connection(self, timeout=5): """ 인터넷 연결 상태를 확인합니다. Args: timeout (int): 연결 시도 시간 제한 (초) Returns: bool: 인터넷 연결 가능 여부 """ import urllib.request import urllib.error import socket test_urls = [ 'https://www.google.com', 'https://www.naver.com', 'https://8.8.8.8', 'https://1.1.1.1' ] for url in test_urls: try: # HTTP 요청으로 연결 테스트 response = urllib.request.urlopen(url, timeout=timeout) if response.getcode() == 200: self.logger.log(f"인터넷 연결 확인됨: {url}", level=logging.DEBUG) return True except (urllib.error.URLError, urllib.error.HTTPError, socket.timeout, OSError) as e: self.logger.log(f"연결 실패 {url}: {str(e)}", level=logging.DEBUG) continue # 모든 URL 테스트 실패 시 소켓 연결로 재시도 try: # Google DNS 서버로 소켓 연결 테스트 sock = socket.create_connection(("8.8.8.8", 53), timeout) sock.close() self.logger.log("소켓 연결로 인터넷 연결 확인됨", level=logging.DEBUG) return True except (socket.error, socket.timeout, OSError) as e: self.logger.log(f"소켓 연결 실패: {str(e)}", level=logging.DEBUG) return False def show_internet_connection_error(self): """ 인터넷 연결 오류 메시지를 표시합니다. """ msg = QMessageBox(self) msg.setIcon(QMessageBox.Warning) msg.setWindowTitle("인터넷 연결 오류") msg.setText("인터넷 연결을 확인할 수 없습니다.") msg.setInformativeText( "비밀번호 변경을 위해서는 인터넷 연결이 필요합니다.\n\n" "다음 사항을 확인해 주세요:\n" "• 네트워크 연결 상태\n" "• 방화벽 설정\n" "• 프록시 설정\n" "• DNS 설정" ) retry_button = msg.addButton("다시 시도", QMessageBox.ActionRole) cancel_button = msg.addButton("취소", QMessageBox.RejectRole) msg.setDefaultButton(retry_button) msg.exec_() if msg.clickedButton() == retry_button: return True # 다시 시도 else: return False # 취소 def init_memory_monitoring(self): """메모리 모니터링을 위한 초기화""" try: # 시스템 전체 메모리 정보 self.system_memory = psutil.virtual_memory() self.total_memory_gb = self.system_memory.total / (1024**3) # GB 단위 # 프로그램 시작시 메모리 사용량 (메인 프로세스만 - 자식 프로세스는 아직 없음) self.process = psutil.Process() self.initial_memory_mb = self.process.memory_info().rss / (1024**2) # MB 단위 self.initial_memory_percent = (self.initial_memory_mb * (1024**2)) / self.system_memory.total * 100 self.baseline_set = False # 첫 번째 작업 시작 시 기준선 설정 # 메모리 업데이트 타이머 self.memory_update_timer = QTimer(self) self.memory_update_timer.timeout.connect(self.update_memory_info) self.memory_update_timer.start(2000) # 2초마다 업데이트 self.logger.log(f"메모리 모니터링 초기화 완료 - 시작시 메모리: {self.initial_memory_mb:.1f}MB ({self.initial_memory_percent:.1f}%)", level=logging.INFO) except Exception as e: self.logger.log(f"메모리 모니터링 초기화 실패: {str(e)}", level=logging.ERROR) def apply_modern_theme(self): """모던하고 현대적인 테마를 적용합니다.""" # 메인 윈도우 스타일 self.setStyleSheet(""" QMainWindow { background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, stop: 0 #f8f9fa, stop: 1 #e9ecef); color: #2c3e50; font-family: 'Segoe UI', 'Arial', sans-serif; } /* 스크롤바 스타일 */ QScrollBar:vertical { background: #f8f9fa; width: 12px; border-radius: 6px; margin: 0px; } QScrollBar::handle:vertical { background: #cbd5e0; border-radius: 6px; min-height: 20px; margin: 2px; } QScrollBar::handle:vertical:hover { background: #a0aec0; } QScrollBar::add-line:vertical, QScrollBar::sub-line:vertical { height: 0px; } /* 메뉴바 스타일 */ QMenuBar { background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, stop: 0 #ffffff, stop: 1 #f8f9fa); border-bottom: 2px solid #e2e8f0; color: #2c3e50; font-weight: 500; } QMenuBar::item { background: transparent; padding: 8px 16px; border-radius: 6px; margin: 2px; } QMenuBar::item:selected { background: #e2e8f0; color: #2c3e50; } /* 상태바 스타일 */ QStatusBar { background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, stop: 0 #ffffff, stop: 1 #f8f9fa); border-top: 1px solid #e2e8f0; color: #64748b; font-size: 9pt; } """) def get_modern_groupbox_style(self, border_color="#3498db", title_color="#2980b9"): """모던한 GroupBox 스타일을 반환합니다.""" return f""" QGroupBox {{ border: 2px solid {border_color}; border-radius: 12px; margin: 15px 0px 10px 0px; padding: 15px 15px 15px 15px; font-size: 11pt; font-weight: 600; color: #2c3e50; background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, stop: 0 #ffffff, stop: 1 #f8f9fa); }} QGroupBox::title {{ subcontrol-origin: margin; left: 20px; padding: 0 12px 0 12px; font-size: 10pt; font-weight: 700; color: {title_color}; background-color: #ffffff; border-radius: 8px; border: 1px solid {border_color}; }} """ def get_modern_button_style(self, color_scheme="blue"): """모던한 버튼 스타일을 반환합니다.""" color_map = { "blue": {"bg": "#3498db", "hover": "#2980b9", "pressed": "#1f5f8b", "text": "white"}, "green": {"bg": "#27ae60", "hover": "#229954", "pressed": "#1e8449", "text": "white"}, "orange": {"bg": "#f39c12", "hover": "#e67e22", "pressed": "#d68910", "text": "#2c3e50"}, "red": {"bg": "#e74c3c", "hover": "#c0392b", "pressed": "#a93226", "text": "white"}, "purple": {"bg": "#9b59b6", "hover": "#8e44ad", "pressed": "#7d3c98", "text": "white"}, "yellow": {"bg": "#f1c40f", "hover": "#f39c12", "pressed": "#f39c12", "text": "#2c3e50"} } colors = color_map.get(color_scheme, color_map["blue"]) return f""" QPushButton {{ background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, stop: 0 {colors['bg']}, stop: 1 {colors['hover']}); border: none; border-radius: 8px; color: {colors.get('text', 'white')}; font-size: 9pt; font-weight: 600; padding: 4px 8px; /* 패딩을 줄여서 텍스트 잘림 방지 */ margin: 2px; text-align: center; }} QPushButton:hover {{ background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, stop: 0 {colors['hover']}, stop: 1 {colors['pressed']}); border: 1px solid {colors['pressed']}; }} QPushButton:pressed {{ background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, stop: 0 {colors['pressed']}, stop: 1 {colors['pressed']}); border: 2px solid {colors['pressed']}; }} QPushButton:disabled {{ background: #bdc3c7; color: #7f8c8d; }} """ def get_modern_progressbar_style(self, color_scheme="blue"): """모던한 프로그레스바 스타일을 반환합니다.""" color_map = { "blue": {"bg": "#3498db", "chunk": "#2980b9"}, "green": {"bg": "#27ae60", "chunk": "#229954"}, "orange": {"bg": "#f39c12", "chunk": "#e67e22"}, "red": {"bg": "#e74c3c", "chunk": "#c0392b"} } colors = color_map.get(color_scheme, color_map["blue"]) return f""" QProgressBar {{ border: 2px solid {colors['bg']}; border-radius: 10px; background-color: #f8f9fa; text-align: center; font-size: 9pt; font-weight: 600; color: #2c3e50; padding: 2px; }} QProgressBar::chunk {{ background: qlineargradient(x1: 0, y1: 0, x2: 1, y2: 0, stop: 0 {colors['bg']}, stop: 1 {colors['chunk']}); border-radius: 8px; }} """ def get_modern_label_style(self, color="#2c3e50", background="transparent"): """모던한 라벨 스타일을 반환합니다.""" return f""" QLabel {{ color: {color}; background-color: {background}; font-size: 9pt; font-weight: 500; padding: 4px; border-radius: 6px; }} """ def get_modern_input_style(self): """모던한 입력 필드 스타일을 반환합니다.""" return """ QLineEdit, QTextEdit, QComboBox { border: 2px solid #e2e8f0; border-radius: 8px; background-color: #ffffff; color: #2c3e50; font-size: 9pt; padding: 8px 12px; selection-background-color: #3498db; } QLineEdit:focus, QTextEdit:focus, QComboBox:focus { border-color: #3498db; background-color: #f8f9fa; } QLineEdit:hover, QTextEdit:hover, QComboBox:hover { border-color: #cbd5e0; } """ def create_memory_monitor_group(self): """메모리 모니터링 그룹 위젯 생성""" memory_group = QGroupBox("시스템 메모리 사용률") memory_group.setStyleSheet(self.get_modern_groupbox_style("#3498db", "#2980b9")) memory_layout = QVBoxLayout() # 메모리 정보 레이블 self.memory_info_label = QLabel() self.memory_info_label.setAlignment(Qt.AlignCenter) self.memory_info_label.setFixedHeight(18) # 라벨 높이 제한 self.memory_info_label.setStyleSheet(""" font-size: 8pt; font-weight: bold; color: #2c3e50; background: rgba(255, 255, 255, 0.9); border-radius: 3px; padding: 2px; """) memory_layout.addWidget(self.memory_info_label) memory_layout.setSpacing(3) # 요소간 간격 줄임 # 통합 프로그레스 바 컨테이너 progress_container = QWidget() progress_layout = QVBoxLayout(progress_container) progress_container.setContentsMargins(0, 0, 0, 0) # 컨테이너 여백 제거 progress_layout.setContentsMargins(5, 5, 5, 5) # 프로그레스 레이아웃 여백 적절히 설정 progress_layout.setSpacing(5) # 간격 적절히 설정 progress_container.setMinimumHeight(35) # 컨테이너 최소 높이 보장 # 통합 메모리 프로그레스 바 unified_memory_layout = QHBoxLayout() # # 메모리 정보 레이블 # memory_info_layout = QVBoxLayout() # memory_info_layout.setSpacing(2) # # 시작시 메모리 정보 # start_info_layout = QHBoxLayout() # start_label = QLabel("시작시:") # start_label.setStyleSheet("font-size: 8pt; font-weight: bold; color: #27ae60;") # start_label.setFixedWidth(50) # start_value = QLabel(f"{self.initial_memory_mb:.0f}MB") # start_value.setStyleSheet("font-size: 8pt; color: #27ae60; font-weight: bold;") # start_value.setFixedWidth(60) # start_info_layout.addWidget(start_label) # start_info_layout.addWidget(start_value) # start_info_layout.addStretch() # # 현재 메모리 정보 # current_info_layout = QHBoxLayout() # current_label = QLabel("현재:") # current_label.setStyleSheet("font-size: 8pt; font-weight: bold; color: #27ae60;") # current_label.setFixedWidth(50) # self.current_value_label = QLabel() # self.current_value_label.setStyleSheet("font-size: 8pt; color: #27ae60; font-weight: bold;") # self.current_value_label.setFixedWidth(60) # current_info_layout.addWidget(current_label) # current_info_layout.addWidget(self.current_value_label) # current_info_layout.addStretch() # # 증가분 정보 # increase_info_layout = QHBoxLayout() # increase_label = QLabel("증가:") # increase_label.setStyleSheet("font-size: 8pt; font-weight: bold; color: #e74c3c;") # increase_label.setFixedWidth(50) # self.increase_value_label = QLabel() # self.increase_value_label.setStyleSheet("font-size: 8pt; color: #e74c3c; font-weight: bold;") # self.increase_value_label.setFixedWidth(60) # increase_info_layout.addWidget(increase_label) # increase_info_layout.addWidget(self.increase_value_label) # increase_info_layout.addStretch() # memory_info_layout.addLayout(start_info_layout) # memory_info_layout.addLayout(current_info_layout) # memory_info_layout.addLayout(increase_info_layout) # 단순한 프로그레스바 (전체 메모리 대비 현재 사용량) self.simple_memory_bar = QProgressBar() self.simple_memory_bar.setMaximum(100) self.simple_memory_bar.setValue(0) # 초기값은 0 self.simple_memory_bar.setTextVisible(True) self.simple_memory_bar.setFixedHeight(25) # 프로그레스바 높이를 약간 증가 self.simple_memory_bar.setFormat("0%") self.simple_memory_bar.setStyleSheet(""" QProgressBar { border: 2px solid #3498db; border-radius: 10px; background-color: #ecf0f1; text-align: center; font-size: 9pt; font-weight: bold; color: #2c3e50; } QProgressBar::chunk { background: qlineargradient(x1: 0, y1: 0, x2: 1, y2: 0, stop: 0 #2ecc71, stop: 0.5 #f39c12, stop: 1 #e74c3c); border-radius: 8px; } """) # unified_memory_layout.addLayout(memory_info_layout) unified_memory_layout.addWidget(self.simple_memory_bar, 1) # 1은 stretch factor progress_layout.addLayout(unified_memory_layout) memory_layout.addWidget(progress_container) memory_group.setLayout(memory_layout) memory_group.setFixedHeight(100) # 프로그레스바가 보이도록 높이 약간 증가 # 초기 메모리 정보 업데이트 self.update_memory_info() return memory_group def update_memory_info(self): """메모리 정보 업데이트""" try: # 메인 프로세스 메모리 사용량 main_memory_mb = self.process.memory_info().rss / (1024**2) # 모든 자식 프로세스의 메모리 사용량 합계 total_memory_mb = main_memory_mb child_processes_info = [] try: # 현재 프로세스의 모든 자식 프로세스 찾기 for child in self.process.children(recursive=True): try: child_memory = child.memory_info().rss / (1024**2) total_memory_mb += child_memory child_processes_info.append((child.pid, child.name(), child_memory)) except (psutil.NoSuchProcess, psutil.AccessDenied, psutil.ZombieProcess): # 프로세스가 이미 종료되었거나 접근 권한이 없는 경우 continue except Exception as e: self.logger.log(f"자식 프로세스 메모리 계산 중 오류: {e}", level=logging.DEBUG) # 디버그용 로그 (자식 프로세스 정보) if child_processes_info: child_info_str = ", ".join([f"{name}({pid}): {mem:.1f}MB" for pid, name, mem in child_processes_info]) # self.logger.log(f"자식 프로세스 메모리: {child_info_str}", level=logging.DEBUG) # 총 메모리 사용량으로 계산 current_memory_mb = total_memory_mb current_memory_percent = (current_memory_mb * (1024**2)) / self.system_memory.total * 100 # 자식 프로세스가 있으면서 아직 기준선이 설정되지 않았다면 현재를 기준선으로 설정 if child_processes_info and not self.baseline_set: self.initial_memory_mb = current_memory_mb self.initial_memory_percent = current_memory_percent self.baseline_set = True # self.logger.log(f"작업 시작 기준선 설정: {self.initial_memory_mb:.1f}MB (메인+자식 프로세스 포함)", level=logging.INFO) # 증가분 계산 increase_mb = current_memory_mb - self.initial_memory_mb increase_percent = current_memory_percent - self.initial_memory_percent # 시스템 전체 메모리 현황 system_memory_current = psutil.virtual_memory() system_used_percent = system_memory_current.percent # 정보 레이블 업데이트 (프로그램 메모리와 시스템 메모리 구분) start_percent = self.initial_memory_percent increase_percent = (increase_mb * (1024**2)) / self.system_memory.total * 100 if increase_mb > 0 else 0 current_program_percent = (current_memory_mb * (1024**2)) / self.system_memory.total * 100 # 메인 + 자식 프로세스 정보 포함 child_count = len(child_processes_info) child_memory_mb = total_memory_mb - main_memory_mb if child_count > 0: info_text = (f"전체: {self.total_memory_gb:.1f}GB | " f"시스템 전체: {system_used_percent:.1f}% | " f"알바생(메인+{child_count}개 작업자): {current_program_percent:.1f}% ({current_memory_mb:.0f}MB)") else: info_text = (f"전체: {self.total_memory_gb:.1f}GB | " f"시스템 전체: {system_used_percent:.1f}% | " f"알바생 사용량: {current_program_percent:.1f}% ({current_memory_mb:.0f}MB)") self.memory_info_label.setText(info_text) # 단순한 프로그레스바 업데이트 (전체 시스템 메모리 대비 현재 사용량) self.simple_memory_bar.setValue(int(system_used_percent)) self.simple_memory_bar.setFormat(f"{system_used_percent:.1f}%") # 시스템 메모리 사용률에 따른 프로그레스바 색상 변경 if system_used_percent >= 80: # 80% 이상 - 빨간색 simple_bar_style = """ QProgressBar { border: 2px solid #e74c3c; border-radius: 10px; background-color: #ecf0f1; text-align: center; font-size: 9pt; font-weight: bold; color: #2c3e50; } QProgressBar::chunk { background: qlineargradient(x1: 0, y1: 0, x2: 1, y2: 0, stop: 0 #e74c3c, stop: 1 #c0392b); border-radius: 8px; } """ elif system_used_percent >= 70: # 70% 이상 - 주황색 simple_bar_style = """ QProgressBar { border: 2px solid #f39c12; border-radius: 10px; background-color: #ecf0f1; text-align: center; font-size: 9pt; font-weight: bold; color: #2c3e50; } QProgressBar::chunk { background: qlineargradient(x1: 0, y1: 0, x2: 1, y2: 0, stop: 0 #f39c12, stop: 1 #e67e22); border-radius: 8px; } """ else: # 60% 이하 - 녹색 simple_bar_style = """ QProgressBar { border: 2px solid #27ae60; border-radius: 10px; background-color: #ecf0f1; text-align: center; font-size: 9pt; font-weight: bold; color: #2c3e50; } QProgressBar::chunk { background: qlineargradient(x1: 0, y1: 0, x2: 1, y2: 0, stop: 0 #2ecc71, stop: 1 #27ae60); border-radius: 8px; } """ self.simple_memory_bar.setStyleSheet(simple_bar_style) except Exception as e: self.logger.log(f"메모리 정보 업데이트 오류: {str(e)}", level=logging.ERROR) @Slot(str, str) def on_manual_login_required(self, market_name: str, message: str): """수동 로그인 요청 처리""" try: self.logger.log(f"수동 로그인 요청: {market_name} - {message}", level=logging.WARNING) # 기존 메시지박스가 있으면 닫기 if self.manual_login_message_box and self.manual_login_message_box.isVisible(): self.manual_login_message_box.close() # QMessageBox로 사용자에게 알림 (non-modal로 변경) self.manual_login_message_box = QMessageBox(self) self.manual_login_message_box.setIcon(QMessageBox.Information) self.manual_login_message_box.setWindowTitle(f"{market_name} 로그인 필요") self.manual_login_message_box.setText(f"{market_name} 로그인이 필요합니다.") self.manual_login_message_box.setDetailedText(message) self.manual_login_message_box.setStandardButtons(QMessageBox.Ok) self.manual_login_message_box.setStyleSheet(""" QMessageBox { background-color: #2b2b2b; color: white; } QMessageBox QPushButton { background-color: #4CAF50; color: white; border: none; padding: 8px 16px; border-radius: 4px; font-weight: bold; } QMessageBox QPushButton:hover { background-color: #45a049; } """) # non-modal로 표시 (사용자가 팝업창에서 로그인할 수 있도록) self.manual_login_message_box.show() except Exception as e: self.logger.log(f"수동 로그인 요청 처리 오류: {str(e)}", level=logging.ERROR) @Slot(str) def on_login_completed(self, market_name: str): """로그인 완료 시 QMessageBox 자동 닫기""" try: self.logger.log(f"로그인 완료 알림: {market_name}", level=logging.INFO) # 수동 로그인 메시지박스가 있고 보이는 상태라면 닫기 if self.manual_login_message_box and self.manual_login_message_box.isVisible(): self.manual_login_message_box.close() self.manual_login_message_box = None self.logger.log(f"✅ {market_name} 수동 로그인 안내창을 자동으로 닫았습니다", level=logging.INFO) except Exception as e: self.logger.log(f"로그인 완료 처리 오류: {str(e)}", level=logging.ERROR) def create_upload_conditions_tab(self): """업로드조건 탭 생성 (등록모드 전용)""" try: # 탭 생성 self.upload_conditions_tab = QWidget() # 메인 레이아웃 main_layout = QVBoxLayout(self.upload_conditions_tab) main_layout.setContentsMargins(20, 20, 20, 20) main_layout.setSpacing(20) # 제목 title_label = QLabel("📤 업로드 조건 설정") title_label.setStyleSheet(""" QLabel { font-size: 16px; font-weight: bold; color: #2c3e50; padding: 10px; background-color: #ecf0f1; border-radius: 8px; border-left: 4px solid #3498db; } """) main_layout.addWidget(title_label) # 설정 그룹 settings_group = QGroupBox("업로드 설정") settings_group.setStyleSheet(""" QGroupBox { font-weight: bold; font-size: 14px; color: #34495e; border: 2px solid #bdc3c7; border-radius: 8px; margin-top: 25px; padding-top: 5px; padding-bottom: 5px; padding-left: 5px; padding-right: 5px; } QGroupBox::title { subcontrol-origin: margin; subcontrol-position: top left; left: 10px; top: -2px; padding: 0 8px 0 8px; background-color: white; } """) settings_layout = QFormLayout(settings_group) settings_layout.setSpacing(25) # 1. 가격범위초과시 대표가격 설정 드롭다운 price_policy_label = QLabel("가격범위초과시 대표가격 설정:") price_policy_label.setStyleSheet("font-weight: bold; color: #2c3e50;") self.price_policy_combo = QComboBox() self.price_policy_combo.addItems([ "1. 최대 업로드 갯수 기준", "2. 최저 옵션가 기준", "3. 상품별 각각의 대표가격 설정" ]) self.price_policy_combo.setCurrentIndex(0) # 기본값: 1번 self.price_policy_combo.setStyleSheet(""" QComboBox { padding: 12px 8px; min-height: 16px; border: 2px solid #bdc3c7; border-radius: 4px; background-color: white; font-size: 12px; } QComboBox:hover { border-color: #3498db; } QComboBox::drop-down { border: none; width: 20px; } QComboBox::down-arrow { image: none; border-left: 5px solid transparent; border-right: 5px solid transparent; border-top: 5px solid #7f8c8d; margin-right: 5px; } """) self.price_policy_combo.currentIndexChanged.connect(self.on_price_policy_changed) settings_layout.addRow(price_policy_label, self.price_policy_combo) # 2. 스스 마켓정책적용 토글 market_policy_label = QLabel("스스 마켓정책적용:") market_policy_label.setStyleSheet("font-weight: bold; color: #2c3e50;") from toggleSwitch import ToggleSwitch self.smartstore_policy_toggle = ToggleSwitch() self.smartstore_policy_toggle.setChecked(True) # 기본값: ON self.smartstore_policy_toggle.clicked.connect(self.on_smartstore_policy_changed) # 토글 위젯을 감싸는 컨테이너 toggle_container = QWidget() toggle_layout = QHBoxLayout(toggle_container) toggle_layout.setContentsMargins(0, 0, 0, 0) toggle_layout.addWidget(self.smartstore_policy_toggle) toggle_layout.addStretch() settings_layout.addRow(market_policy_label, toggle_container) main_layout.addWidget(settings_group) # 설명 라벨 description_label = QLabel(""" 💡 설정 안내: • 가격범위초과시 대표가격: 상품의 옵션 가격이 설정 범위를 초과할 때 사용할 대표가격 결정 방식 • 스스 마켓정책적용: 스마트스토어 마켓 정책을 자동으로 적용할지 여부 """) description_label.setStyleSheet(""" QLabel { background-color: #f8f9fa; border: 1px solid #dee2e6; border-radius: 6px; padding: 12px; font-size: 11px; color: #495057; line-height: 1.4; } """) description_label.setWordWrap(True) main_layout.addWidget(description_label) main_layout.addStretch() # 탭 위젯에 추가 self.toggle_tab_widget.addTab(self.upload_conditions_tab, "업로드조건") # 초기 설정값 로드 self.load_upload_conditions_settings() # GUI 초기화 완료 후 설정값 다시 로드 (보장) QTimer.singleShot(100, self.load_upload_conditions_settings) self.logger.log("업로드조건 탭 생성 완료", level=logging.INFO) except Exception as e: self.logger.log(f"업로드조건 탭 생성 오류: {str(e)}", level=logging.ERROR) def on_price_policy_changed(self, index): """가격정책 드롭다운 변경 핸들러""" try: self.upload_price_policy = index + 1 # 1, 2, 3으로 저장 self.save_upload_conditions_settings() self.logger.log(f"가격정책 변경: {self.upload_price_policy}", level=logging.DEBUG) except Exception as e: self.logger.log(f"가격정책 변경 오류: {str(e)}", level=logging.ERROR) def on_smartstore_policy_changed(self, checked): """스마트스토어 정책 토글 변경 핸들러""" try: self.smartstore_market_policy = checked self.save_upload_conditions_settings() self.logger.log(f"스마트스토어 마켓정책 변경: {self.smartstore_market_policy}", level=logging.DEBUG) except Exception as e: self.logger.log(f"스마트스토어 정책 변경 오류: {str(e)}", level=logging.ERROR) def save_upload_conditions_settings(self): """업로드조건 설정 저장""" try: self.settings_manager.save_value("upload_conditions/price_policy", self.upload_price_policy) self.settings_manager.save_value("upload_conditions/smartstore_market_policy", self.smartstore_market_policy) self.logger.log("업로드조건 설정 저장 완료", level=logging.DEBUG) except Exception as e: self.logger.log(f"업로드조건 설정 저장 오류: {str(e)}", level=logging.ERROR) def load_upload_conditions_settings(self): """업로드조건 설정 불러오기 (UI 반영)""" try: # settings에서 최신값 다시 로드 (다른 곳에서 변경되었을 수 있음) self.upload_price_policy = self.settings_manager.get_value("upload_conditions/price_policy", 1) self.smartstore_market_policy = self.settings_manager.get_value("upload_conditions/smartstore_market_policy", True) # UI에 반영 (위젯이 생성된 후에만) if hasattr(self, 'price_policy_combo') and self.price_policy_combo is not None: # 시그널 차단 후 설정 (무한 루프 방지) self.price_policy_combo.blockSignals(True) self.price_policy_combo.setCurrentIndex(self.upload_price_policy - 1) # 0, 1, 2로 변환 self.price_policy_combo.blockSignals(False) self.logger.log(f"가격정책 UI 반영: {self.upload_price_policy} -> index {self.upload_price_policy - 1}", level=logging.DEBUG) if hasattr(self, 'smartstore_policy_toggle') and self.smartstore_policy_toggle is not None: # 시그널 차단 후 설정 (무한 루프 방지) self.smartstore_policy_toggle.blockSignals(True) self.smartstore_policy_toggle.setChecked(self.smartstore_market_policy) self.smartstore_policy_toggle.blockSignals(False) self.logger.log(f"스마트스토어정책 UI 반영: {self.smartstore_market_policy}", level=logging.DEBUG) self.logger.log(f"업로드조건 설정 불러오기 완료 - 가격정책: {self.upload_price_policy}, 스스정책: {self.smartstore_market_policy}", level=logging.DEBUG) except Exception as e: self.logger.log(f"업로드조건 설정 불러오기 오류: {str(e)}", level=logging.ERROR)