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()}
🔄 진행 과정:
위 설정으로 업로드를 시작하시겠습니까?
(작업 시작 후에는 되돌릴 수 없습니다)
📦 현재 그룹: {current_group}
🔢 총 상품수: {self.total_product_count}개
⚙️ 설정 모드: 기존 마켓 설정 유지
🔄 진행 과정:
업로드를 시작하시겠습니까?
(작업 시작 후에는 되돌릴 수 없습니다)
이미지 번역에 사용할 폰트를 선택합니다.
버튼을 눌러 폰트 갤러리에서 미리보기를 확인하고 선택하세요.
""" ) 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, """옵션명 순서를 다양한 방식으로 넘버링합니다.
옵션 이미지에 포함된 중국어 텍스트를 한국어로 번역하여 새 이미지를 생성합니다.
옵션 이미지의 사이즈 표, 색상 설명 등을 번역하여 고객에게 제공합니다.
옵션 이미지의 특성에 따라 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을 입력합니다.
웹훅 생성 방법:
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 모델을 선택합니다:
자체서버의 종류를 선택합니다:
메인서버: 실제 운영용 메인 서버
테스트서버: 개발 및 테스트용 서버
서버 종류에 따라 연결되는 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"""🧪 실제 DirectML 추론 테스트: 성공
테스트 시간: {test_duration:.2f}초
감지된 GPU: {gpu_info.get('model_name', 'Unknown')}
벤더: {vendor_text} | 타입: {gpu_type_text}
환경: {vm_status}
📋 GPU 모드 사용 권장
모든 번역 설정에서 'GPU' 모드를 선택하세요!
🚀 실제 GPU 가속이 정상 동작합니다. 번역 속도가 크게 향상됩니다!
🖥️ VM 환경 감지됨
Proxmox, VMware, VirtualBox 등의 가상화 환경에서는
GPU 패스스루 설정이 필요할 수 있습니다.
🧪 DirectML 추론 테스트: 실패
{error_details}
📋 권장 설정
모든 번역 설정에서 'CPU' 모드를 사용하세요
가능한 원인:
안전한 CPU 모드로 동작하며, 품질은 동일합니다.
{feature_name}은(는) VIP 멤버십에서만 사용할 수 있습니다.
VIP 멤버십으로 업그레이드하시면:
VIP 멤버십에 대한 자세한 정보는 고객센터에 문의해주세요.