diff --git a/browser_control.py b/browser_control.py index 7bd14897..088cda0e 100644 --- a/browser_control.py +++ b/browser_control.py @@ -137,7 +137,7 @@ class BrowserController(QThread): # self.imageProcessor = ImageProcessor(self.logger, self.page, self.whale_translator, self.clipboardImageManager, self.TEMP_IMAGE_DIR, self.toggle_states) # ImageProcessor를 포함하여 다른 핸들러들 초기화 - self.optionHandler = OptionHandler(self.locator_manager, self, self.TEMP_IMAGE_DIR, self.logger, self.gpt_client, self.update_detail_progress_signal, self.set_progress_visible_signal, toggle_states=self.toggle_states) + self.optionHandler = OptionHandler(self.locator_manager, self, self.TEMP_IMAGE_DIR, self.logger, self.gpt_client, update_detail_progress_signal=self.update_detail_progress_signal, set_progress_visible_signal=self.set_progress_visible_signal, toggle_states=self.toggle_states) self.priceHandler = PriceHandler(self.locator_manager, self, self.logger, self.optionHandler, self.price_setting_diag, self.toggle_states, debug_flag=self.toggle_states['debug_mode']) self.thumbnailHandler = ThumbnailHandler(self.locator_manager, self, self.logger, self.toggle_states, self.update_detail_progress_signal, self.set_progress_visible_signal, self.base_path) # self.titleGenerator = TitleGenerator(self.locator_manager, self, self.logger, self.whale_translator, self.toggle_states, self.gpt_client, self.forbidden_word_manager, self.user_id, self.supabase_manager) @@ -333,8 +333,8 @@ class BrowserController(QThread): try: close_btn = page.locator(self.welcome_popup_closeBTN_selector) - close_btn.wait_for(state='visible', timeout=timeout_sec * 1000) - close_btn.click() + await close_btn.wait_for(state='visible', timeout=timeout_sec * 1000) + await close_btn.click() self.logger.log("환영 다이얼로그를 닫았습니다.", level=logging.INFO) except TimeoutError: @@ -342,9 +342,9 @@ class BrowserController(QThread): self.logger.log("환영 다이얼로그가 감지되지 않았습니다.", level=logging.INFO) finally: - page.keyboard.press('Escape') + await page.keyboard.press('Escape') await asyncio.sleep(0.53) - page.keyboard.press('Escape') + await page.keyboard.press('Escape') async def start_browser_async(self): @@ -582,7 +582,7 @@ class BrowserController(QThread): try: # 각 핸들러에 초기화된 page 객체 전달. self.titleGenerator.update_page(self.page ,self.toggle_states) - self.titleGenerator.update_parsing_page(self.parsing_page) + # self.titleGenerator.update_parsing_page(self.parsing_page) self.optionHandler.update_page(self.page ,self.toggle_states) self.tagsHandler.update_page(self.page ,self.toggle_states) self.thumbnailHandler.update_page(self.page ,self.toggle_states) @@ -1917,7 +1917,6 @@ class BrowserController(QThread): self.logger.log(f"현재페이지 : [{current_page_number}]", level=logging.INFO) next_page_number = current_page_number + 1 - # 다음 페이지 버튼을 찾음 (title 속성으로 다음 페이지를 찾음) next_page_button_locator = self.next_page_button_template.format(page_number=next_page_number) next_page_button = await self.page.query_selector(next_page_button_locator) @@ -2168,35 +2167,35 @@ class BrowserController(QThread): self.logger.log(f"detail_IMGTrans_type : {detail_IMGTrans_type}", level=logging.DEBUG) self.logger.log(f"thumb_trans_type : {thumb_trans_type}", level=logging.DEBUG) - if optionIMGTrans_type or detail_IMGTrans_type or thumb_trans_type: - self.logger.log('이미지 번역 타입이 웨일로 설정되어 있습니다. 웨일 브라우저를 실행합니다...', level=logging.DEBUG) + # if optionIMGTrans_type or detail_IMGTrans_type or thumb_trans_type: + # self.logger.log('이미지 번역 타입이 웨일로 설정되어 있습니다. 웨일 브라우저를 실행합니다...', level=logging.DEBUG) - # 웨일 브라우저 시작 - 최대 3번 재시도 - max_retries = 3 - retry_count = 0 - whale_window = None + # # 웨일 브라우저 시작 - 최대 3번 재시도 + # max_retries = 3 + # retry_count = 0 + # whale_window = None - while retry_count < max_retries: - self.check_pause() # 일시중지 상태 확인 - self.logger.log(f'웨일 브라우저 시작 시도 {retry_count + 1}/{max_retries}...', level=logging.INFO) - whale_window = self.whale_translator.start_trans_browser() + # while retry_count < max_retries: + # self.check_pause() # 일시중지 상태 확인 + # self.logger.log(f'웨일 브라우저 시작 시도 {retry_count + 1}/{max_retries}...', level=logging.INFO) + # whale_window = self.whale_translator.start_trans_browser() - if whale_window: - self.logger.log('웨일 브라우저가 성공적으로 시작되었습니다.', level=logging.INFO) - break + # if whale_window: + # self.logger.log('웨일 브라우저가 성공적으로 시작되었습니다.', level=logging.INFO) + # break - retry_count += 1 - self.logger.log(f'웨일 브라우저 시작 실패. {retry_count}/{max_retries}', level=logging.WARNING) - await asyncio.sleep(1) # 잠시 대기 후 재시도 + # retry_count += 1 + # self.logger.log(f'웨일 브라우저 시작 실패. {retry_count}/{max_retries}', level=logging.WARNING) + # await asyncio.sleep(1) # 잠시 대기 후 재시도 - if not whale_window: - error_msg = "웨일 브라우저를 시작할 수 없습니다. 상품수정을 중단합니다." - self.logger.log(error_msg, level=logging.ERROR) - self.translation_error.emit(error_msg) - return + # if not whale_window: + # error_msg = "웨일 브라우저를 시작할 수 없습니다. 상품수정을 중단합니다." + # self.logger.log(error_msg, level=logging.ERROR) + # self.translation_error.emit(error_msg) + # return - if whale_window and self.toggle_states['collect_method_combo'] == "lens": - self.whale_translator.check_capcha() + # if whale_window and self.toggle_states['collect_method_combo'] == "lens": + # self.whale_translator.check_capcha() # 1. 총 상품 수 수집 self.check_pause() # 일시중지 상태 확인 @@ -3034,3 +3033,4 @@ class BrowserController(QThread): # 약간의 랜덤 대기 후 포커스 복원 await asyncio.sleep(random.uniform(0.03, 0.13)) await self.restore_focus(page, orig_focus) + diff --git a/login_dialog.py b/login_dialog.py index ba2d0114..db61e765 100644 --- a/login_dialog.py +++ b/login_dialog.py @@ -355,7 +355,8 @@ class LoginDialog(QDialog): QMessageBox.warning(self, "오류", "사용자 정보를 가져오지 못했습니다.") return else: - self.logger.log(f"로그인 성공 full_user_info : {full_user_info}", level=logging.DEBUG) + # self.logger.log(f"로그인 성공 full_user_info : {full_user_info}", level=logging.DEBUG) + self.logger.log(f"로그인 성공", level=logging.DEBUG) # 멤버십 레벨에 따른 최대 세션 수 설정 membership_data = full_user_info.get("membership_level_data", {}) @@ -571,7 +572,7 @@ class LoginDialog(QDialog): def get_user_info(self): """로그인 또는 회원가입에 성공한 사용자 정보를 반환합니다.""" - self.logger.log(f"full_user_info : {self.user}", level=logging.DEBUG) + # self.logger.log(f"full_user_info : {self.user}", level=logging.DEBUG) return self.user def get_update_selection_data(self): diff --git a/mainUI_SP.py b/mainUI_SP.py index 598a8873..58315c92 100644 --- a/mainUI_SP.py +++ b/mainUI_SP.py @@ -1065,24 +1065,24 @@ class MAIN_GUI(QMainWindow): def on_detailIMGTrans_type_toggle_clicked(self, checked): # False == "대체" (Off) - if not checked: - QMessageBox.information(self, "안내", "아직 대체 이미지 번역은 지원하지 않습니다.\nAPI 이미지 번역으로 자동 변경됩니다.") - self.detail_IMGTrans_type_toggle.setChecked(True) - self.universal_input_handler(self.detail_IMGTrans_type_toggle, True) # 항상 네(True)로 처리 + # if not checked: + QMessageBox.information(self, "안내", "웨일 번역 패치로 인해 대체 이미지 번역만 사용가능합니다.") + self.detail_IMGTrans_type_toggle.setChecked(False) + # self.universal_input_handler(self.detail_IMGTrans_type_toggle, False) # 항상 네(True)로 처리 def on_thumb_trans_type_toggle_clicked(self, checked): # False == "대체" (Off) # if not checked: - QMessageBox.information(self, "안내", "API 이미지 번역은 조기종료되었습니다.\n대체 이미지 번역으로 자동 변경됩니다.") + QMessageBox.information(self, "안내", "웨일 번역 패치로 인해 대체 이미지 번역만 사용가능합니다.") self.thumb_trans_type_toggle.setChecked(False) - self.universal_input_handler(self.thumb_trans_type_toggle, False) # 항상 대체(False)로 처리 + # self.universal_input_handler(self.thumb_trans_type_toggle, False) # 항상 대체(False)로 처리 def on_optionIMGTrans_type_toggle_clicked(self, checked): # False == "대체" (Off) - if not checked: - QMessageBox.information(self, "안내", "아직 대체 이미지 번역은 지원하지 않습니다.\nAPI 이미지 번역으로 자동 변경됩니다.") - self.optionIMGTrans_type_toggle.setChecked(True) - self.universal_input_handler(self.optionIMGTrans_type_toggle, True) # 항상 네(True)로 처리 + # if not checked: + QMessageBox.information(self, "안내", "웨일 번역 패치로 인해 대체 이미지 번역만 사용가능합니다.") + self.optionIMGTrans_type_toggle.setChecked(False) + # self.universal_input_handler(self.optionIMGTrans_type_toggle, False) # 항상 네(True)로 처리 def universal_input_handler(self, widget, *args): """ @@ -1511,6 +1511,10 @@ class MAIN_GUI(QMainWindow): # self.thumb_trans_type_toggle.setChecked(False) self.load_unwanted_words() + self.optionIMGTrans_type_toggle.setChecked(False) + self.detail_IMGTrans_type_toggle.setChecked(False) + self.thumb_trans_type_toggle.setChecked(False) + # self.admin_toggle.setChecked(False) # time.sleep(0.1) # self.admin_toggle.setChecked(True) @@ -2222,9 +2226,9 @@ class MAIN_GUI(QMainWindow): # 토글 버튼 그룹 self.global_toggle_group = QGroupBox("기능") self.global_toggle_layout = QVBoxLayout(self.global_toggle_group) - self.set_group_style(self.global_toggle_group, self.global_toggle_layout, "neumorphism") + # self.set_group_style(self.global_toggle_group, self.global_toggle_layout, "neumorphism") # self.global_toggle_layout.setContentsMargins(10, 10, 10, 10) - self.global_toggle_layout.setSpacing(10) + # self.global_toggle_layout.setSpacing(10) # 설명 그룹 self.global_manual_group = QGroupBox("기능 매뉴얼") @@ -2356,7 +2360,7 @@ class MAIN_GUI(QMainWindow): self.memo_widget2.enterEvent = lambda e: self.show_manual_html( self.global_manual_group, - "📜 메모 입력 옵션션", + "📜 메모 입력 옵션", self.global_manual_label, "메모 입력 옵션을 설정합니다.
" "메모순서 : 기존 사용자메모가 있을경우 어떤 메모를 먼저 사용할지 선택합니다.
" @@ -2418,7 +2422,7 @@ class MAIN_GUI(QMainWindow): self.unwanted_words_button_label = QLabel('OCR 설정', self) self.unwanted_words_button = QPushButton('OCR', self) self.unwanted_words_button.setObjectName("unwanted_words_button") - self.unwanted_words_button.setFixedWidth(30) + # self.unwanted_words_button.setFixedWidth(30) self.set_PUSHBTN_style(self.unwanted_words_button,"modern") self.unwanted_words_button.clicked.connect(self.on_unwanted_words_button_clicked) self.unwanted_words_button.setEnabled(False) # 초기 상태는 비활성화 @@ -2443,9 +2447,9 @@ class MAIN_GUI(QMainWindow): # 번역간 대기시간 self.interval_widget = QWidget() self.interval_spinbox_layout = QHBoxLayout(self.interval_widget) - self.interval_spinbox_label = QLabel("번역간 대기", self)# 작업 완료 메서드 수정 + self.interval_spinbox_label = QLabel("번역간격", self)# 작업 완료 메서드 수정 self.interval_spinbox = QSpinBox(self) - self.interval_spinbox.setFixedHeight(30) + # self.interval_spinbox.setFixedHeight(30) self.interval_spinbox.setObjectName("interval") self.interval_spinbox.setRange(2, 10) self.interval_spinbox.setValue(3) @@ -2456,27 +2460,27 @@ class MAIN_GUI(QMainWindow): self.interval_spinbox.valueChanged.connect(lambda value: self.universal_input_handler(self.interval_spinbox, value)) self.interval_widget.enterEvent = lambda e: self.show_manual_html( self.global_manual_group, - "대체번역시 번역간 간격", + "번역 사이 간격 및 번역완료대기", self.global_manual_label, - "대체번역시 번역사이 시간간격을 설정합니다.
" + "번역간격
" + "번역간격 : 번역사이 시간간격을 설정합니다.
" "최소 2초, 최대 10초까지 설정 가능합니다. 기본값은 3초입니다.
" "설정된 시간의 50% ~ 200% 사이의 랜덤시간으로 휴식합니다.
" "설정된 시간이 짧으면 서버에 많은 부담을 주어 매우매우 좋지않은 일이 일어날수 있습니다.
" + "번역완료대기
" + "대체번역시 번역이 완료될때까지 대기 시간을 설정합니다.
" + "최소 5초, 최대 60초까지 설정 가능합니다. 기본값은 20초입니다.
" + "설정된 시간이 길어도 번역이 끝나면 완료처리됩니다.
" + "그러나 서버에 부하가 몰리거나 번역속도가 느릴 경우 전체 번역 시간이 증가할 수 있습니다.
" + + ) self.interval_widget.leaveEvent = lambda e: self.reset_manual(self.global_manual_group, self.global_manual_label) - self.interval_spinbox_layout.addWidget(self.interval_spinbox_label) - self.interval_spinbox_layout.addWidget(self.interval_spinbox) - - self.global_toggle_layout.addWidget(self.interval_widget) - - # 번역대기시간 스핀 - self.watingTime_widget = QWidget() - self.watingTime_spinbox_layout = QHBoxLayout(self.watingTime_widget) - self.watingTime_spinbox_label = QLabel("번역대기시간", self)# 작업 완료 메서드 수정 + self.watingTime_spinbox_label = QLabel("번역대기", self)# 작업 완료 메서드 수정 self.watingTime_spinbox = QSpinBox(self) - self.watingTime_spinbox.setFixedHeight(30) + # self.watingTime_spinbox.setFixedHeight(30) self.watingTime_spinbox.setObjectName("watingTime") self.watingTime_spinbox.setRange(5, 60) self.watingTime_spinbox.setValue(20) @@ -2485,43 +2489,78 @@ class MAIN_GUI(QMainWindow): # 번역대기시간 스핀에 대한 독립적인 이벤트 핸들러 생성 self.watingTime_spinbox.valueChanged.connect(lambda value: self.universal_input_handler(self.watingTime_spinbox, value)) - self.watingTime_widget.enterEvent = lambda e: self.show_manual_html( - self.global_manual_group, - "번역시 대기기시간", - self.global_manual_label, - "대체번역시 번역이 완료될때까지 대기 시간을 설정합니다.
" - "최소 5초, 최대 60초까지 설정 가능합니다. 기본값은 20초입니다.
" - "설정된 시간이 길어도 번역이 끝나면 완료처리됩니다.
" - "그러나 서버에 부하가 몰리거나 번역속도가 느릴 경우 전체 번역 시간이 증가할 수 있습니다.
" + # self.watingTime_widget.enterEvent = lambda e: self.show_manual_html( + # self.global_manual_group, + # "번역시 대기기시간", + # self.global_manual_label, + # "대체번역시 번역이 완료될때까지 대기 시간을 설정합니다.
" + # "최소 5초, 최대 60초까지 설정 가능합니다. 기본값은 20초입니다.
" + # "설정된 시간이 길어도 번역이 끝나면 완료처리됩니다.
" + # "그러나 서버에 부하가 몰리거나 번역속도가 느릴 경우 전체 번역 시간이 증가할 수 있습니다.
" - ) - self.watingTime_widget.leaveEvent = lambda e: self.reset_manual(self.global_manual_group, self.global_manual_label) + # ) + # self.watingTime_widget.leaveEvent = lambda e: self.reset_manual(self.global_manual_group, self.global_manual_label) - self.watingTime_spinbox_layout.addWidget(self.watingTime_spinbox_label) - self.watingTime_spinbox_layout.addWidget(self.watingTime_spinbox) - self.global_toggle_layout.addWidget(self.watingTime_widget) - # GPT 모델설정 - self.gpt_model_widget = QWidget() - self.gpt_model_layout = QHBoxLayout(self.gpt_model_widget) - self.gpt_model_label = QLabel("GPT 모델", self) - self.gpt_model_combo = QComboBox(self) - self.gpt_model_combo.addItems(["gpt-4o", "gpt-4o-mini"]) - self.gpt_model_combo.setCurrentIndex(0) + self.interval_spinbox_layout.addWidget(self.interval_spinbox_label) + self.interval_spinbox_layout.addWidget(self.interval_spinbox) + self.interval_spinbox_layout.addWidget(self.watingTime_spinbox_label) + self.interval_spinbox_layout.addWidget(self.watingTime_spinbox) - self.gpt_model_widget.enterEvent = lambda e: self.show_manual_html( - self.global_manual_group, - "🔢 GPT 모델 설정", - self.global_manual_label, - "GPT 모델을 설정합니다." - ) - self.gpt_model_widget.leaveEvent = lambda e: self.reset_manual(self.global_manual_group, self.global_manual_label) + self.global_toggle_layout.addWidget(self.interval_widget) - self.gpt_model_layout.addWidget(self.gpt_model_label) - self.gpt_model_layout.addWidget(self.gpt_model_combo) + # # 번역대기시간 스핀 + # self.watingTime_widget = QWidget() + # self.watingTime_spinbox_layout = QHBoxLayout(self.watingTime_widget) + # self.watingTime_spinbox_label = QLabel("번역대기", self)# 작업 완료 메서드 수정 + # self.watingTime_spinbox = QSpinBox(self) + # # self.watingTime_spinbox.setFixedHeight(30) + # self.watingTime_spinbox.setObjectName("watingTime") + # self.watingTime_spinbox.setRange(5, 60) + # self.watingTime_spinbox.setValue(20) + # self.watingTime_spinbox.setSuffix("Second") + # self.watingTime_spinbox.setFixedWidth(100) + + # # 번역대기시간 스핀에 대한 독립적인 이벤트 핸들러 생성 + # self.watingTime_spinbox.valueChanged.connect(lambda value: self.universal_input_handler(self.watingTime_spinbox, value)) + # self.watingTime_widget.enterEvent = lambda e: self.show_manual_html( + # self.global_manual_group, + # "번역시 대기기시간", + # self.global_manual_label, + # "대체번역시 번역이 완료될때까지 대기 시간을 설정합니다.
" + # "최소 5초, 최대 60초까지 설정 가능합니다. 기본값은 20초입니다.
" + # "설정된 시간이 길어도 번역이 끝나면 완료처리됩니다.
" + # "그러나 서버에 부하가 몰리거나 번역속도가 느릴 경우 전체 번역 시간이 증가할 수 있습니다.
" - self.global_toggle_layout.addWidget(self.gpt_model_widget) + # ) + # self.watingTime_widget.leaveEvent = lambda e: self.reset_manual(self.global_manual_group, self.global_manual_label) + + # self.watingTime_spinbox_layout.addWidget(self.watingTime_spinbox_label) + # self.watingTime_spinbox_layout.addWidget(self.watingTime_spinbox) + + # self.global_toggle_layout.addWidget(self.watingTime_widget) + + # # GPT 모델설정 + # self.gpt_model_widget = QWidget() + # self.gpt_model_layout = QHBoxLayout(self.gpt_model_widget) + # self.gpt_model_label = QLabel("GPT 모델", self) + # self.gpt_model_combo = QComboBox(self) + # self.gpt_model_combo.addItems(["gpt-4o", "gpt-4o-mini"]) + # self.gpt_model_combo.setCurrentIndex(0) + + # self.gpt_model_widget.enterEvent = lambda e: self.show_manual_html( + # self.global_manual_group, + # "🔢 GPT 모델 설정", + # self.global_manual_label, + # "GPT 모델을 설정합니다." + # ) + # self.gpt_model_widget.leaveEvent = lambda e: self.reset_manual(self.global_manual_group, self.global_manual_label) + + # self.gpt_model_layout.addWidget(self.gpt_model_label) + # self.gpt_model_layout.addWidget(self.gpt_model_combo) + + # self.global_toggle_layout.addWidget(self.gpt_model_widget) # 디버그 모드 토글 @@ -2619,6 +2658,7 @@ class MAIN_GUI(QMainWindow):

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

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

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

+

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

""" ) self.title_widget.leaveEvent = lambda e: self.reset_manual(self.product_name_manual_group, self.product_name_manual_label) @@ -2875,6 +2915,9 @@ class MAIN_GUI(QMainWindow): self.optionIMGTrans_toggle_label = QLabel("옵션 이미지번역", self) self.optionIMGTrans_toggle = ToggleSwitch(self) self.optionIMGTrans_toggle.setObjectName("optionIMGTrans_toggle") + self.optionIMGTrans_toggle.clicked.connect + + self.optionIMGTrans_toggle.clicked.connect(lambda checked: self.universal_input_handler(self.optionIMGTrans_toggle, checked)) self.optionIMGTrans_widget.enterEvent = lambda e: self.show_manual_html( self.option_manual_group, @@ -2899,12 +2942,12 @@ class MAIN_GUI(QMainWindow): self.optionIMGTrans_type_toggle_label = QLabel("옵션 이미지번역 타입", self) self.optionIMGTrans_type_toggle = ToggleSwitch(self) self.optionIMGTrans_type_toggle.setObjectName("optionIMGTrans_type_toggle") - self.optionIMGTrans_type_toggle.setChecked(True) - # self.optionIMGTrans_type_toggle.clicked.connect(self.on_optionIMGTrans_type_toggle_clicked) + self.optionIMGTrans_type_toggle.setChecked(False) + self.optionIMGTrans_type_toggle.clicked.connect(self.on_optionIMGTrans_type_toggle_clicked) # self.optionIMGTrans_type_toggle.clicked.connect(lambda checked: self.universal_input_handler(self.optionIMGTrans_type_toggle, checked)) self.optionIMGTrans_type_toggle.setOnText("API") self.optionIMGTrans_type_toggle.setOffText("대체") - self.optionIMGTrans_type_toggle.setEnabled(False) + # self.optionIMGTrans_type_toggle.setEnabled(False) self.optionIMGTrans_type_widget.enterEvent = lambda e: self.show_manual_html( self.option_manual_group, "🔄 옵션 이미지번역 타입", @@ -3310,11 +3353,12 @@ class MAIN_GUI(QMainWindow): self.thumb_trans_type_toggle_label = QLabel("썸네일번역 타입", self) self.thumb_trans_type_toggle = ToggleSwitch(self) self.thumb_trans_type_toggle.setObjectName("thumb_trans_type_toggle") - # self.thumb_trans_type_toggle.clicked.connect(self.on_thumb_trans_type_toggle_clicked) + self.thumb_trans_type_toggle.setChecked(False) + self.thumb_trans_type_toggle.clicked.connect(self.on_thumb_trans_type_toggle_clicked) # self.thumb_trans_type_toggle.clicked.connect(lambda checked: self.universal_input_handler(self.thumb_trans_type_toggle, checked)) self.thumb_trans_type_toggle.setOnText("API") self.thumb_trans_type_toggle.setOffText("대체") - self.thumb_trans_type_toggle.setEnabled(False) + # self.thumb_trans_type_toggle.setEnabled(False) self.thumb_trans_type_widget.enterEvent = lambda e: self.show_manual_html( self.thumbnail_manual_group, "🔄 썸네일 번역 타입", @@ -3522,11 +3566,11 @@ class MAIN_GUI(QMainWindow): self.detail_IMGTrans_type_toggle = ToggleSwitch(self) self.detail_IMGTrans_type_toggle.setChecked(True) self.detail_IMGTrans_type_toggle.setObjectName("detail_IMGTrans_type_toggle") - # self.detail_IMGTrans_type_toggle.clicked.connect(self.on_detailIMGTrans_type_toggle_clicked) + self.detail_IMGTrans_type_toggle.clicked.connect(self.on_detailIMGTrans_type_toggle_clicked) # self.detail_IMGTrans_type_toggle.clicked.connect(lambda checked: self.universal_input_handler(self.detail_IMGTrans_type_toggle, checked)) self.detail_IMGTrans_type_toggle.setOnText("API") self.detail_IMGTrans_type_toggle.setOffText("대체") - self.detail_IMGTrans_type_toggle.setEnabled(False) + # self.detail_IMGTrans_type_toggle.setEnabled(False) self.detail_IMGTrans_type_widget.enterEvent = lambda e: self.show_manual_html( self.detail_manual_group, "🔄 상세페이지 번역 타입", @@ -4636,6 +4680,32 @@ class MAIN_GUI(QMainWindow): 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) diff --git a/src/contents/details.py b/src/contents/details.py index d9afe668..036b30b7 100644 --- a/src/contents/details.py +++ b/src/contents/details.py @@ -375,9 +375,11 @@ class DetailHandler: return False # 상세페이지 옵션에 따라 처리 + self.logger.log(f"self.detail_Option : {self.detail_Option}", level=logging.DEBUG) + self.logger.log(f"self.detail_IMGTrans : {self.detail_IMGTrans}", level=logging.DEBUG) if self.detail_Option or self.detail_IMGTrans: - if self.detail_Option and not self.detail_IMGTrans: + if self.detail_Option: self.logger.log("소개글 입력 시작...", level=logging.INFO) await self.input_detail_text(optionHandler) diff --git a/src/contents/option.py b/src/contents/option.py index e7c5f34c..75a3bf69 100644 --- a/src/contents/option.py +++ b/src/contents/option.py @@ -5,7 +5,7 @@ import os import logging import random class OptionHandler: - def __init__(self, locator_manager, browser_controller, whale_translator, clipboardImageManager, TEMP_IMAGE_DIR, logger, gpt_client, update_detail_progress_signal, set_progress_visible_signal, toggle_states, imageProcessor): + def __init__(self, locator_manager, browser_controller, TEMP_IMAGE_DIR, logger, gpt_client, update_detail_progress_signal, set_progress_visible_signal, toggle_states): self.update_detail_progress_signal = update_detail_progress_signal self.set_progress_visible_signal = set_progress_visible_signal @@ -13,7 +13,7 @@ class OptionHandler: self.locator_manager = locator_manager self.browser_controller = browser_controller self.page = self.browser_controller.page - self.clipboardImageManager = clipboardImageManager + # self.clipboardImageManager = clipboardImageManager self.TEMP_IMAGE_DIR = TEMP_IMAGE_DIR self.logger = logger @@ -22,8 +22,8 @@ class OptionHandler: # self.interval = self.toggle_states['interval'] self.gpt_client = gpt_client - self.whale_translator = whale_translator - self.imageProcessor = imageProcessor + # self.whale_translator = whale_translator + # self.imageProcessor = imageProcessor self.is_percenty_success = False self.is_gpt_success = False @@ -92,8 +92,9 @@ class OptionHandler: self.price_inside_box = 'sup' - def update_page(self, page1): + def update_page(self, page1, toggle_states1): self.page = page1 + self.toggle_states = toggle_states1 self.logger.log(f"page객체 업데이트 : {page1}", level=logging.DEBUG) # def update_whale(self): @@ -347,15 +348,6 @@ class OptionHandler: self.is_gpt_success = False self.is_percenty_success = True - # except google.api_core.exceptions.ResourceExhausted as re: - # # 할당량 초과 예외 처리 - # self.logger.log(f"Vertex AI 할당량 초과: {re}", level=logging.ERROR) - # self.logger.log("퍼센티 자체 AI번역 사용 시도", level=logging.DEBUG) - # pyautogui.hotkey('alt', 'q') - # self.logger.log("번역을 위한 5초간 대기", level=logging.DEBUG) - # time.sleep(5) - # translation_success = False # 번역 실패 - except Exception as e: # 기타 예외 처리 self.logger.log(f"번역 처리 중 알 수 없는 오류 발생: {e}", level=logging.ERROR, exc_info=True) @@ -765,13 +757,6 @@ class OptionHandler: # 필터링된 옵션명만 추출 filtered_option_names = {option['name'] for option in filtered_options} - # # 필터링된 옵션들만 체크박스에서 남기고 나머지 체크박스 해제 - # checkboxes = [ - # self.option_info['checkboxes'][i] - # for i, name in enumerate(self.option_info['original_names'].values()) - # if name in filtered_option_names - # ] - # 체크박스 상태 조정 await self.adjust_options(filtered_option_names, max_option_count) @@ -858,163 +843,163 @@ class OptionHandler: await self.page.click(self.low_order_button_locator) - async def update_option_image_to_whale(self, toggle_states, debug_flag=False): - """ - 옵션 이미지가 존재할 경우, 제외된 옵션이 아닌 경우 번역하여 업데이트하는 메서드. + # async def update_option_image_to_whale(self, toggle_states, debug_flag=False): + # """ + # 옵션 이미지가 존재할 경우, 제외된 옵션이 아닌 경우 번역하여 업데이트하는 메서드. - :param debug_flag: 디버그 모드일 경우 임시 이미지를 삭제하지 않음 (기본값 False). - """ - # self.is_option_wm = False + # :param debug_flag: 디버그 모드일 경우 임시 이미지를 삭제하지 않음 (기본값 False). + # """ + # # self.is_option_wm = False - try: - # 모든 옵션 상자 요소 가져오기 - option_boxes = await self.page.query_selector_all(self.option_box_selector) + # try: + # # 모든 옵션 상자 요소 가져오기 + # option_boxes = await self.page.query_selector_all(self.option_box_selector) - # option_boxes = await self.page.query_selector_all("div#productMainContentContainerId li > div > div:nth-child(1) > div > div:nth-child(2) > div") + # # option_boxes = await self.page.query_selector_all("div#productMainContentContainerId li > div > div:nth-child(1) > div > div:nth-child(2) > div") - # option_image_element = await self.page.locator("xpath=//*[@id='productMainContentContainerId']/div[1]/div[2]/div/div/div[2]/div/div[1]/div/div/div[2]/div/div/div[5]/div[1]/div/div/ul/li[1]/div/div[1]/div/div[2]/div/img").element_handle() + # # option_image_element = await self.page.locator("xpath=//*[@id='productMainContentContainerId']/div[1]/div[2]/div/div/div[2]/div/div[1]/div/div/div[2]/div/div/div[5]/div[1]/div/div/ul/li[1]/div/div[1]/div/div[2]/div/img").element_handle() - total_options = len(option_boxes) - self.logger.log(f"총 {total_options}개의 옵션 이미지 번역을 시작합니다.", level=logging.DEBUG) + # total_options = len(option_boxes) + # self.logger.log(f"총 {total_options}개의 옵션 이미지 번역을 시작합니다.", level=logging.DEBUG) - # 실제 옵션 이미지가 존재하는 항목에만 인덱스를 적용하기 위해 별도 카운터 사용 - translated_index = 1 - total_option_boxes = len(option_boxes) + # # 실제 옵션 이미지가 존재하는 항목에만 인덱스를 적용하기 위해 별도 카운터 사용 + # translated_index = 1 + # total_option_boxes = len(option_boxes) - self.set_progress_visible_signal.emit(True) + # self.set_progress_visible_signal.emit(True) - # 각 옵션 상자를 순회 - for index, option_box in enumerate(option_boxes, start=1): - # 선택자에서 인덱스를 반영해 동적 선택자를 생성 - add_button_selector = self.add_button_selector.format(index=index) + # # 각 옵션 상자를 순회 + # for index, option_box in enumerate(option_boxes, start=1): + # # 선택자에서 인덱스를 반영해 동적 선택자를 생성 + # add_button_selector = self.add_button_selector.format(index=index) - # 옵션 박스 내부의 텍스트 확인하여 "제외된 옵션" 포함 여부 검사 - option_text_content = await option_box.inner_text() - if "제외된 옵션" in option_text_content: - self.logger.log(f"{index}번째 옵션은 제외된 옵션입니다. 번역을 생략합니다.", level=logging.DEBUG) - continue # 제외된 옵션이므로 다음 옵션으로 이동 + # # 옵션 박스 내부의 텍스트 확인하여 "제외된 옵션" 포함 여부 검사 + # option_text_content = await option_box.inner_text() + # if "제외된 옵션" in option_text_content: + # self.logger.log(f"{index}번째 옵션은 제외된 옵션입니다. 번역을 생략합니다.", level=logging.DEBUG) + # continue # 제외된 옵션이므로 다음 옵션으로 이동 - # 옵션 이미지가 존재하는지 확인 - option_image = await option_box.query_selector("img") - if option_image is None: - self.logger.log(f"{index}번째 옵션에 이미지가 없습니다. 다음 옵션으로 이동합니다.", level=logging.DEBUG) - continue + # # 옵션 이미지가 존재하는지 확인 + # option_image = await option_box.query_selector("img") + # if option_image is None: + # self.logger.log(f"{index}번째 옵션에 이미지가 없습니다. 다음 옵션으로 이동합니다.", level=logging.DEBUG) + # continue - option_image_url = await option_image.get_attribute("src") - self.logger.log(f"{index}번째 옵션 이미지 URL: {option_image_url}", level=logging.DEBUG) + # option_image_url = await option_image.get_attribute("src") + # self.logger.log(f"{index}번째 옵션 이미지 URL: {option_image_url}", level=logging.DEBUG) - # 이미지가 SVG 형식일 경우 번역을 건너뜀 - if option_image_url.endswith(".svg"): - self.logger.log(f"{index}번째 옵션은 SVG 이미지입니다. 번역을 생략합니다.", level=logging.DEBUG) - continue + # # 이미지가 SVG 형식일 경우 번역을 건너뜀 + # if option_image_url.endswith(".svg"): + # self.logger.log(f"{index}번째 옵션은 SVG 이미지입니다. 번역을 생략합니다.", level=logging.DEBUG) + # continue - try: - # ImageProcessor를 사용하여 이미지 처리 - self.logger.log(f"{index}번째 옵션의 이미지 처리 시도", level=logging.DEBUG) + # try: + # # ImageProcessor를 사용하여 이미지 처리 + # self.logger.log(f"{index}번째 옵션의 이미지 처리 시도", level=logging.DEBUG) - # option 구분자를 포함한 임시파일명으로 처리 - final_image_path = await self.imageProcessor.process_single_image( - option_image_url, translated_index-1, self.toggle_states.get('is_localServer', False), file_prefix="option" - ) + # # option 구분자를 포함한 임시파일명으로 처리 + # final_image_path = await self.imageProcessor.process_single_image( + # option_image_url, translated_index-1, self.toggle_states.get('is_localServer', False), file_prefix="option" + # ) - if final_image_path is False: - # 불필요한 키워드 포함으로 제외된 이미지 - self.logger.log(f"{index}번째 옵션 이미지 제외됨 (불필요한 키워드 포함)", level=logging.INFO) - continue + # if final_image_path is False: + # # 불필요한 키워드 포함으로 제외된 이미지 + # self.logger.log(f"{index}번째 옵션 이미지 제외됨 (불필요한 키워드 포함)", level=logging.INFO) + # continue - elif isinstance(final_image_path, str) and final_image_path and os.path.exists(final_image_path): - # 정상적으로 처리된 이미지 사용 - translated_image_path = final_image_path - self.logger.log(f"{index}번째 옵션의 이미지 처리 완료: {translated_image_path}", level=logging.DEBUG) - else: - # 예상하지 못한 반환값 또는 파일이 존재하지 않음 - self.logger.log(f"{index}번째 옵션 이미지 처리 결과 예상하지 못한 값: {final_image_path}", level=logging.WARNING) - continue + # elif isinstance(final_image_path, str) and final_image_path and os.path.exists(final_image_path): + # # 정상적으로 처리된 이미지 사용 + # translated_image_path = final_image_path + # self.logger.log(f"{index}번째 옵션의 이미지 처리 완료: {translated_image_path}", level=logging.DEBUG) + # else: + # # 예상하지 못한 반환값 또는 파일이 존재하지 않음 + # self.logger.log(f"{index}번째 옵션 이미지 처리 결과 예상하지 못한 값: {final_image_path}", level=logging.WARNING) + # continue - # self.browser_controller.switch_to_chrome() # 크롬으로 포커스 이동 + # # self.browser_controller.switch_to_chrome() # 크롬으로 포커스 이동 - if os.path.exists(translated_image_path): - # 삭제 버튼 클릭 - # delete_button = await self.page.query_selector(delete_button_selector) + # if os.path.exists(translated_image_path): + # # 삭제 버튼 클릭 + # # delete_button = await self.page.query_selector(delete_button_selector) - self.logger.log(f"{index}번째 옵션의 이미지 삭제 버튼 가져오기", level=logging.DEBUG) + # self.logger.log(f"{index}번째 옵션의 이미지 삭제 버튼 가져오기", level=logging.DEBUG) - try: - # 기본 선택자로 삭제 버튼 찾기 - delete_button = self.page.locator(f'{self.delete_button_selector_template.format(index=index)}') - await delete_button.wait_for(state="attached", timeout=5000) # 타임아웃 설정 + # try: + # # 기본 선택자로 삭제 버튼 찾기 + # delete_button = self.page.locator(f'{self.delete_button_selector_template.format(index=index)}') + # await delete_button.wait_for(state="attached", timeout=5000) # 타임아웃 설정 - if not await delete_button.is_visible(): - # fallback으로 재시도 - delete_button = self.page.locator(f'xpath={self.fallback1_delete_button_selector_template.format(index=index)}') - await delete_button.wait_for(state="attached", timeout=5000) + # if not await delete_button.is_visible(): + # # fallback으로 재시도 + # delete_button = self.page.locator(f'xpath={self.fallback1_delete_button_selector_template.format(index=index)}') + # await delete_button.wait_for(state="attached", timeout=5000) - if await delete_button.is_visible(): - await delete_button.click() - self.logger.log(f"{index}번째 옵션의 삭제 버튼 클릭", level=logging.DEBUG) + # if await delete_button.is_visible(): + # await delete_button.click() + # self.logger.log(f"{index}번째 옵션의 삭제 버튼 클릭", level=logging.DEBUG) - # 다이알로그 확인 후 삭제 버튼 클릭 - try: - self.logger.log(f"{index}번째 옵션의 삭제 다이알로그에서 삭제 버튼 클릭 시도...", level=logging.DEBUG) - deleted = await self.click_option_image_delete_button() - if deleted: - self.logger.log(f"{index}번째 옵션의 삭제 다이알로그 삭제버튼 클릭 성공", level=logging.DEBUG) - else: - self.logger.log(f"{index}번째 옵션의 삭제 다이알로그 삭제버튼 클릭 실패", level=logging.ERROR) - except Exception as e: - self.logger.log(f"{index}번째 옵션의 삭제 다이알로그를 찾거나 삭제버튼 클릭 중 오류: {e}", level=logging.ERROR, exc_info=True) + # # 다이알로그 확인 후 삭제 버튼 클릭 + # try: + # self.logger.log(f"{index}번째 옵션의 삭제 다이알로그에서 삭제 버튼 클릭 시도...", level=logging.DEBUG) + # deleted = await self.click_option_image_delete_button() + # if deleted: + # self.logger.log(f"{index}번째 옵션의 삭제 다이알로그 삭제버튼 클릭 성공", level=logging.DEBUG) + # else: + # self.logger.log(f"{index}번째 옵션의 삭제 다이알로그 삭제버튼 클릭 실패", level=logging.ERROR) + # except Exception as e: + # self.logger.log(f"{index}번째 옵션의 삭제 다이알로그를 찾거나 삭제버튼 클릭 중 오류: {e}", level=logging.ERROR, exc_info=True) - except Exception as e: - self.logger.log(f"{index}번째 옵션의 삭제 버튼을 찾는 중 오류 발생: {e}", level=logging.ERROR, exc_info=True) + # except Exception as e: + # self.logger.log(f"{index}번째 옵션의 삭제 버튼을 찾는 중 오류 발생: {e}", level=logging.ERROR, exc_info=True) - try: - # '+ 버튼' 클릭 후 파일 업로드 - self.logger.log(f"{index}번째 옵션의 이미지추가 버튼 가져오기", level=logging.DEBUG) - add_button = await self.page.query_selector(add_button_selector) + # try: + # # '+ 버튼' 클릭 후 파일 업로드 + # self.logger.log(f"{index}번째 옵션의 이미지추가 버튼 가져오기", level=logging.DEBUG) + # add_button = await self.page.query_selector(add_button_selector) - if add_button: - await add_button.click() - self.logger.log(f"{index}번째 옵션의 이미지추가 버튼 클릭", level=logging.DEBUG) + # if add_button: + # await add_button.click() + # self.logger.log(f"{index}번째 옵션의 이미지추가 버튼 클릭", level=logging.DEBUG) - # 파일 업로드 영역의 input 요소 직접 선택 (수정된 부분) - file_input = await self.page.query_selector(self.file_upload_button_selector) # Ant Design의 클래스 사용 + # # 파일 업로드 영역의 input 요소 직접 선택 (수정된 부분) + # file_input = await self.page.query_selector(self.file_upload_button_selector) # Ant Design의 클래스 사용 - if file_input: - # Playwright의 set_input_files를 사용하여 파일 업로드 처리 - await file_input.set_input_files(translated_image_path) - self.logger.log(f"{index}번째 옵션의 파일 업로드 완료", level=logging.DEBUG) + # if file_input: + # # Playwright의 set_input_files를 사용하여 파일 업로드 처리 + # await file_input.set_input_files(translated_image_path) + # self.logger.log(f"{index}번째 옵션의 파일 업로드 완료", level=logging.DEBUG) - # '이미지 삽입' 버튼 클릭 - confirm_upload_button = await self.page.wait_for_selector(self.confirm_upload_button_selector) - await confirm_upload_button.click() - self.logger.log(f"{index}번째 옵션에 이미지가 업로드되었습니다.", level=logging.DEBUG) - else: - self.logger.log(f"{index}번째 옵션의 파일 입력 요소를 찾을 수 없습니다.", level=logging.ERROR) - except Exception as e: - self.logger.log(f"{index}번째 옵션의 이미지를 추가하는 중 오류 발생: {e}", level=logging.ERROR, exc_info=True) + # # '이미지 삽입' 버튼 클릭 + # confirm_upload_button = await self.page.wait_for_selector(self.confirm_upload_button_selector) + # await confirm_upload_button.click() + # self.logger.log(f"{index}번째 옵션에 이미지가 업로드되었습니다.", level=logging.DEBUG) + # else: + # self.logger.log(f"{index}번째 옵션의 파일 입력 요소를 찾을 수 없습니다.", level=logging.ERROR) + # except Exception as e: + # self.logger.log(f"{index}번째 옵션의 이미지를 추가하는 중 오류 발생: {e}", level=logging.ERROR, exc_info=True) - except Exception as e: - self.logger.log(f"{index}번째 옵션 이미지 처리 중 오류 발생: {e}", level=logging.ERROR, exc_info=True) + # except Exception as e: + # self.logger.log(f"{index}번째 옵션 이미지 처리 중 오류 발생: {e}", level=logging.ERROR, exc_info=True) - finally: - # 파일 사용 후 0.5초 대기하여 접근 완료 보장 - time.sleep(0.5) + # finally: + # # 파일 사용 후 0.5초 대기하여 접근 완료 보장 + # time.sleep(0.5) - pywinauto.clipboard.EmptyClipboard() + # pywinauto.clipboard.EmptyClipboard() - time.sleep(0.5) - # 디버그 모드가 아닐 경우 임시 파일 삭제는 ImageProcessor에서 관리되므로 생략 - # (ImageProcessor가 자체적으로 임시 파일을 관리함) + # time.sleep(0.5) + # # 디버그 모드가 아닐 경우 임시 파일 삭제는 ImageProcessor에서 관리되므로 생략 + # # (ImageProcessor가 자체적으로 임시 파일을 관리함) - # 실제 번역이 완료된 경우에만 인덱스 증가 - translated_index += 1 - self.update_detail_progress_signal.emit(index, total_option_boxes) + # # 실제 번역이 완료된 경우에만 인덱스 증가 + # translated_index += 1 + # self.update_detail_progress_signal.emit(index, total_option_boxes) - self.set_progress_visible_signal.emit(False) + # self.set_progress_visible_signal.emit(False) - except Exception as e: - self.logger.log(f"옵션 이미지 업데이트 중 오류 발생: {e}", level=logging.ERROR, exc_info=True) + # except Exception as e: + # self.logger.log(f"옵션 이미지 업데이트 중 오류 발생: {e}", level=logging.ERROR, exc_info=True) # 파일 삭제를 위한 재시도 함수 def try_delete_file(self, file_path, retries=5, wait_time=0.5): diff --git a/src/contents/tags.py b/src/contents/tags.py index 0abe022b..98a1065c 100644 --- a/src/contents/tags.py +++ b/src/contents/tags.py @@ -71,7 +71,9 @@ class TagsHandler: self.logger.log(f"원본 keyword_tags List : {keyword_tags}", level=logging.DEBUG) if tag_ai and gpt_client: # 태그 키워드 리스트 추출 - keyword_tags = gpt_client.generate_product_name_and_tags(title_infos.get("keyword_tags", []), title_infos.get("original_name", ""), title_infos.get("top_titles", []), title_infos.get("category", ""), title_infos.get("base_prompt", "")) + # keyword_tags = gpt_client.generate_product_name_and_tags(title_infos.get("keyword_tags", []), title_infos.get("original_name", ""), title_infos.get("top_titles", []), title_infos.get("category", ""), title_infos.get("base_prompt", "")) + name, tags = gpt_client.generate_product_name_and_tags(title_infos.get("keyword_tags", []), title_infos.get("original_name", ""), title_infos.get("top_titles", []), title_infos.get("category", ""), title_infos.get("base_prompt", "")) + keyword_tags = tags # 튜플에서 태그 부분만 사용 if not keyword_tags: self.logger.log("gpt 태그가 비어 있습니다.", level=logging.WARNING) return diff --git a/src/contents/옵션.md b/src/contents/옵션.md index c98d2079..2cba9163 100644 --- a/src/contents/옵션.md +++ b/src/contents/옵션.md @@ -13,9 +13,44 @@ div#productMainContentContainerId div[aria-expanded='true'][role='button'] span. "옵션 타입 02" + + +self.checkbox_selector_template = #productMainContentContainerId li:nth-child({index}) input[type="checkbox"] +self.total_options_selector = #productMainContentContainerId label.ant-checkbox-wrapper +self.single_option_locator = //div[@id="productMainContentContainerId"]//label[contains(@class, 'ant-radio-button-wrapper-checked') and contains(., '단일 상품등록')] + + +self.original_name_selector_template =div#productMainContentContainerId li:nth-child({index}) > div > div:nth-child(1) > div > div:nth-child(3) > div:nth-child(3) > span +self.edit_field_selector_template = div#productMainContentContainerId li:nth-child({index}) > div > div:nth-child(1) > div > div:nth-child(3) > div:nth-child(2) > div:nth-child(1) > span > input +self.checkbox_selector_template = #productMainContentContainerId li:nth-child({index}) input[type="checkbox"] +self.image_selector_template = div#productMainContentContainerId li:nth-child({index}) > div > div:nth-child(1) > div > div:nth-child(2) > div > img +self.price_selector_template = //*[@id="productMainContentContainerId"]/div[1]/div[2]/div/div/div[2]/div/div[1]/div/div/div[2]/div/div/div[5]/div[1]/div/div/ul/li[{index}]/div/div[1]/div/div[3]/div[1]/div[2]/button/span/sup + + + 옵션타입1의 옵션 체크박스들의 셀럭터 div#productMainContentContainerId li[aria-describedby='DndDescribedBy-0'] input.ant-checkbox-input[type='checkbox'] +옵션타입2의 옵션 체크박스들의 셀럭터 +div#productMainContentContainerId li[aria-describedby='DndDescribedBy-1'] input.ant-checkbox-input[type='checkbox'] + +옵션타입3의 옵션 체크박스들의 셀럭터 +div#productMainContentContainerId li[aria-describedby='DndDescribedBy-2'] input.ant-checkbox-input[type='checkbox'] + + +옵션타입1의 옵션1의 요소(체크상태) +
  • 썸네일로 복사
    삭제
    편집
    옵션명
    16/25
    8代太空舱白【强力全壁刷】【强力去污】强力电机
  • + +옵션타입1의 옵션1의 요소(체크 해제상태) +
  • 썸네일로 복사
    삭제
    편집
    제외된 옵션
    옵션명
    16/25
    8代太空舱白【强力全壁刷】【强力去污】强力电机
  • + + +체크되었을때 요소 상태 + +체크되지 않았을때 요소 상태 + + + 옵션타입 1 중 체크되었을때 요소의 셀럭터 div#productMainContentContainerId li[aria-describedby='DndDescribedBy-0'] input.ant-checkbox-input[checked=''][type='checkbox'] @@ -27,3 +62,15 @@ div#productMainContentContainerId li[aria-describedby='DndDescribedBy-0'] input. 옵션타입2의 옵션 체크박스들의 셀럭터 div#productMainContentContainerId li[aria-describedby='DndDescribedBy-1'] input.ant-checkbox-input[checked=''][type='checkbox'] + + +1. 옵션타입이 + + + + + + + + +
    \ No newline at end of file diff --git a/src/contents/옵션수정.md b/src/contents/옵션수정.md new file mode 100644 index 00000000..a9c816a6 --- /dev/null +++ b/src/contents/옵션수정.md @@ -0,0 +1,449 @@ +상품수정 자동화 중이야. 파이썬 playwright를 사용하지. + +아래의 기존코드를 개선하고 옵션선택방법 및 옵션정보수집등 전체적으로 개선하고 싶어. + +우선 선택자들은 xpath나 css등이 섞여있고, 변화에 취약해. 그리고 순서로만 하다보니 매우 불안정해. + +1. 옵션타입별로 상품정보를 수집해야 해. +2. 선택자들은 아래처럼 div#productMainContentContainerId내부에 모두 존재해. 이게 상품수정 다이알로그야. 이 중 현재 옵션탭에서 수정중이야. +단일 상품일수도 있고 옵션상품일수도 있어. +옵션상품이라면 옵션타입1은 무조건 존재해 +각 옵션타입 내부에는 각종 버튼들이 있어. 이 버튼들을 등록해줘. +[aria-describedby='DndDescribedBy-0'] + + +[기존 코드] + async def is_single_option(self): + """단일 상품 상태 여부를 확인하는 메서드""" + try: + # 단일 상품 등록 버튼이 선택되었는지 확인 + single_option_checked = await self.page.query_selector(self.single_option_locator) is not None + # 옵션 상품 등록 버튼이 선택되었는지 확인 + option_product_checked = await self.page.query_selector(self.option_product_locator) is not None + + # 두 요소의 상태를 기반으로 단일 상품 여부 결정 + is_single = single_option_checked and not option_product_checked + self.logger.log(f"단일 상품 여부: {'단일 상품입니다' if is_single else '옵션 상품입니다'}", level=logging.DEBUG) + return is_single + except Exception as e: + self.logger.log(f"단일 옵션 확인 중 예외 발생: {e}", level=logging.ERROR, exc_info=True) + return False + + + async def is_all_options_checked(self, click_to_check_to_all=False): + """ + 전체 옵션 체크박스 상태를 확인하고 필요한 경우 전체 체크 상태로 변경. + + Args: + click_to_check_to_all (bool): 전체 옵션을 체크할지 여부. True면 전체 체크 상태로 변경. + + Returns: + bool: 전체 체크 상태일 경우 True, 일부 또는 전체 체크 해제 상태일 경우 False. + """ + try: + # 전체 체크박스 요소 가져오기 + checkbox_element = await self.page.query_selector(self.is_all_option_checked_selector) + if not checkbox_element: + self.logger.log("전체 체크박스 요소를 찾을 수 없습니다.", level=logging.ERROR) + return False + + # 체크박스 상태 확인 + aria_checked = await checkbox_element.get_attribute('aria-checked') + self.logger.log(f"aria_checked : {aria_checked}----------------", level=logging.DEBUG) + + + # 체크박스 상태 판단 + if aria_checked == 'mixed': + self.logger.log("전체 체크박스가 일부만 체크되어 있음", level=logging.DEBUG) + + if click_to_check_to_all: + # 전체 체크 수행 + await checkbox_element.click() + self.logger.log("전체 체크박스를 전체 체크 상태로 변경", level=logging.DEBUG) + return True # 전체 체크 후 True 반환 + else: + return False # 일부 체크 상태만 확인 시 False 반환 + + elif aria_checked == 'true' or aria_checked is None: + self.logger.log("전체 체크박스가 완전 체크 상태임", level=logging.DEBUG) + return True + + else: + self.logger.log("전체 체크박스가 체크 해제 상태임", level=logging.DEBUG) + return False + + except Exception as e: + self.logger.log(f"전체 옵션 체크박스 상태 확인 중 오류 발생: {e}", level=logging.ERROR, exc_info=True) + return False + + async def process_options(self, product_name, forbidden_manager, toggle_states=False): + """ + 옵션 처리 로직. 옵션을 번역하고 이미지를 업데이트함. + + :param product_name: 상품명 (str 형태). + :param max_option_count: 최대 옵션 갯수 (기본값 20). + """ + try: + # 옵션 처리 시작 시 이전 상태 초기화 + self.reset_state() + + # self.update_whale() + + self.logger.log(f"상품명: {product_name}에 대한 옵션을 처리 중...", level=logging.DEBUG) + + self.logger.log(f"이전 상품의 옵션정보를 초기화합니다.", level=logging.DEBUG) + self.init_option_info() + + max_option_count = toggle_states.get('max_option_count', 0) + + # self.logger.log(f"포커스를 위해 클릭1회", level=logging.DEBUG) + # self.low_order_click() + + # self.logger.log(f"휠스크롤 Down", level=logging.DEBUG) + # await self.browser_controller.scroll_with_wheel('down') + # self.logger.log(f"휠스크롤 Up", level=logging.DEBUG) + # await self.browser_controller.scroll_with_wheel('up') + + # await self.page.wait_for_load_state('domcontentloaded') + # self.logger.log(f"동적요소 로딩완료", level=logging.DEBUG) + await asyncio.sleep(2) + + # 1. 단일 옵션인지 판단 + if await self.is_single_option(): + self.logger.log("단일 옵션 상품입니다. 옵션 수정 과정을 생략합니다.", level=logging.DEBUG) + self.option_info['is_single_option'] = True + + return self.option_info + + # 2. 전체 옵션 체크박스 상태 확인 + click_to_check_to_all = True + + self.logger.log(f"일부 체크된 옵션상품에 대한 처리 방법 : {'전체체크에서 시작' if click_to_check_to_all else '일부 체크시 수정완료로 판단'}", level=logging.DEBUG) + + if click_to_check_to_all: + self.logger.log("옵션이 일부만 체크된 상태입니다. 전체 체크로 바꿉니다.", level=logging.DEBUG) + await self.is_all_options_checked(click_to_check_to_all) + else: + self.logger.log("옵션이 일부만 체크된 상태입니다. 옵션 수정이 완료된 상품으로 판단하여 패스합니다.", level=logging.DEBUG) + self.option_info['is_completed_option'] = True + + return self.option_info + + # 3. 가격 낮은 순 정렬 클릭 + await self.low_order_click() + + + # 4. 옵션 정보 수집 + try: + self.logger.log(f"옵션 정보 수집", level=logging.INFO) + self.option_info = await self.collect_options_info() + self.logger.log(f"수집된 옵션 정보 : {self.option_info}", level=logging.DEBUG) + except Exception as e: + # 옵션 처리 중 오류 발생 시 전체 로그 출력 + self.logger.log(f"옵션 정보 수집 중 오류 발생: {e}", level=logging.ERROR, exc_info=True) + + + # 5. 옵션 번역 + optionTrnas = toggle_states.get('optionTrnas') + self.logger.log(f"optionTrnas : {optionTrnas}", level=logging.DEBUG) + if optionTrnas: + self.logger.log(f"옵션 AI번역 : {optionTrnas}", level=logging.DEBUG) + + try: + # gpt를 통한 번역 시도 + translated_options = self.gpt_client.translate_options(self.option_info.get('original_names'), product_name) + self.logger.log(f"번역된 옵션 입력", level=logging.DEBUG) + await self.apply_translated_options(translated_options, self.option_info['edit_fields'], forbidden_manager) + + self.is_gpt_success = True # 번역 성공 + + except ValueError as ve: + # 안전 필터 예외 처리 + if "SAFETY" in str(ve): + self.logger.log(f"안전 필터에 의해 번역 요청이 차단되었습니다. {ve}", level=logging.ERROR) + self.logger.log("퍼센티 자체 AI번역 사용 시도", level=logging.DEBUG) + await self.page.click(self.ai_option_btn_selector) + self.logger.log("번역을 위한 5초간 대기", level=logging.DEBUG) + await asyncio.sleep(5) + self.is_gpt_success = False + self.is_percenty_success = True + + except Exception as e: + # 기타 예외 처리 + self.logger.log(f"번역 처리 중 알 수 없는 오류 발생: {e}", level=logging.ERROR, exc_info=True) + self.logger.log("퍼센티 자체 AI번역 사용 시도", level=logging.DEBUG) + await self.page.click(self.ai_option_btn_selector) + self.logger.log("번역을 위한 5초간 대기", level=logging.DEBUG) + await asyncio.sleep(5) + self.is_gpt_success = False # 번역 실패 + self.is_percenty_success = True + + # 번역 성공 여부에 따른 로그 + self.logger.log(f"[{'GPT_AI' if self.is_gpt_success else '퍼센티AI'}] 를 이용한 옵션번역 성공", level=logging.DEBUG) + + + # 5. 옵션 필터링 및 조정 + optionAutoSelect = toggle_states.get('optionAutoSelect') + if optionAutoSelect: + self.logger.log(f"옵션 필터링 및 조정 : {optionAutoSelect}", level=logging.DEBUG) + await self.filter_and_adjust_options(max_option_count) + # 가격 낮은 순 재정렬 클릭 + await self.low_order_click() + + # 6. 선택된 옵션정보 재수집 + # if self.is_percenty_success: + self.logger.log(f"옵션정보 재수집", level=logging.DEBUG) + await self.store_selected_options() # 페이지에서 실제 선택된 옵션을 수집하여 저장 + + # 7. 옵션 이미지 업데이트 (옵션 이미지가 있는 경우) + if toggle_states.get('optionIMGTrans'): + self.logger.log(f"옵션 이미지 번역을 시작합니다.", level=logging.DEBUG) + + # if toggle_states.get('optionIMGTrans_type'): + # await self.update_option_image_to_whale(toggle_states, debug_flag=self.debug_flag) + # else: + await self.update_option_image_to_per(toggle_states) + + # 8. A-Z or 1-99 button 클릭 + + what_prefix_button = '1-99' + + if what_prefix_button == 'A-Z': + await self.AtoZ_button_click() + elif what_prefix_button == '1-99': + # # 9. A-Z 버튼 클릭 + await self.one_to_nine_button_click() + + await self.low_order_click() + + # 9. 저장 버튼 클릭 + self.logger.log("저장 버튼을 클릭합니다.", level=logging.DEBUG) + await self.page.click('button:has-text("저장하기")') + + self.logger.log("옵션 처리 완료.", level=logging.DEBUG) + return self.option_info + + except Exception as e: + self.logger.log(f"옵션 처리 중 오류 발생: {e}", level=logging.DEBUG, exc_info=True) + return None + + + async def collect_options_info(self): + """옵션 정보를 수집 (이미지, 옵션명, 편집 필드, 가격, 체크박스 정보 포함)""" + try: + # 총 옵션 갯수 수집 + total_options_element = await self.page.query_selector(self.total_options_selector) + total_options_count = int(''.join(filter(str.isdigit, await total_options_element.inner_text()))) if total_options_element else 0 + + self.logger.log(f"총 옵션 갯수: {total_options_count}", level=logging.DEBUG) + + # 옵션 정보를 비동기로 수집 (각 항목 병렬 처리) + for i in range(1, total_options_count + 1): + try: + original_name_selector = self.original_name_selector_template.format(index=i) + edit_field_selector = self.edit_field_selector_template.format(index=i) + checkbox_selector = self.checkbox_selector_template.format(index=i) + image_selector = self.image_selector_template.format(index=i) + price_selector = self.price_selector_template.format(index=i) + + tasks = [ + self.page.query_selector(original_name_selector), + self.page.query_selector(edit_field_selector), + self.page.query_selector(checkbox_selector), + self.page.query_selector(image_selector), + self.page.query_selector(price_selector) + ] + + elements = await asyncio.gather(*tasks) + original_name_element, edit_field_element, checkbox_element, image_element, price_element = elements + + self.logger.log(f"{i}번째 original_name_element : {original_name_element}", level=logging.DEBUG) + self.logger.log(f"{i}번째 edit_field_element : {edit_field_element}", level=logging.DEBUG) + self.logger.log(f"{i}번째 checkbox_element : {checkbox_element}", level=logging.DEBUG) + self.logger.log(f"{i}번째 image_element : {image_element}", level=logging.DEBUG) + self.logger.log(f"{i}번째 price_element : {price_element}", level=logging.DEBUG) + + if original_name_element: + original_name = await original_name_element.inner_text() + self.option_info['original_names'][f'origin_option_{i}'] = original_name + self.option_info['edit_fields'][original_name] = edit_field_element if edit_field_element else None + self.option_info['checkboxes'].append(checkbox_element if checkbox_element else None) + + # 체크박스 상태 수집 (체크 여부는 클래스 이름으로 판단) + checkbox_state = None + + if checkbox_element: + checkbox_classes = await checkbox_element.get_attribute('class') + if 'ant-checkbox-checked' in checkbox_classes: + checkbox_state = True + elif 'ant-checkbox' in checkbox_classes: + checkbox_state = False + + self.option_info['checkboxes'].append({'option_name': original_name, 'checked': checkbox_state}) + + self.logger.log(f"=============================================", level=logging.DEBUG) + self.logger.log(f"{i}번째 옵션 checkbox_state : {checkbox_state}", level=logging.DEBUG) + self.logger.log(f"=============================================", level=logging.DEBUG) + + if image_element: + self.option_info['images'][original_name] = await image_element.get_attribute('src') + if price_element: + price_text = await price_element.inner_text() + price = [int(p.replace(",", "")) for p in price_text.replace("원", "").split(" - ")] + self.option_info['prices'][original_name] = {'low_price': price[0], 'high_price': price[-1]} + + self.logger.log(f"{i}번째 옵션 정보 수집 완료", level=logging.DEBUG) + except Exception as e: + self.logger.log(f"{i}번째 옵션 수집 중 오류 발생: {e}", level=logging.ERROR, exc_info=True) + except Exception as e: + self.logger.log(f"옵션 정보 수집 중 오류 발생: {e}", level=logging.ERROR, exc_info=True) + return self.option_info + + async def filter_and_adjust_options(self, max_option_count): + """가격 필터링을 적용하고 옵션을 조정""" + try: + # 옵션 가격 정보 수집 + prices = { + name: {'low_price': info['low_price'], 'high_price': info['high_price']} + for name, info in self.option_info['prices'].items() + } + + # 가격 정보 형식에 맞게 옵션들을 리스트로 변환 + options_list = [ + { + "name": name, + "price": self.round_to_UP((info['low_price'] + info['high_price']) / 2) # 중간 값을 사용하여 필터링 + } + for name, info in prices.items() + ] + + # 미끼 상품 필터링 및 가격 범위 재조정 + filtered_options = self.filter_bait_items_with_price_distribution(options_list) + + # 필터링된 옵션명만 추출 + filtered_option_names = {option['name'] for option in filtered_options} + + # 체크박스 상태 조정 + await self.adjust_options(filtered_option_names, max_option_count) + + except Exception as e: + self.logger.log(f"옵션 필터링 및 조정 중 오류 발생: {e}", level=logging.ERROR, exc_info=True) + + async def adjust_options(self, filtered_option_names, max_option_count): + """ + 필터링된 옵션에 맞게 체크박스 상태를 조정하는 메서드. + :param filtered_option_names: 필터링된 옵션 리스트 + :param max_option_count: 최대 선택 가능한 옵션 수 (0이면 제한 없음) + """ + try: + selected_count = 0 # 현재까지 선택된 옵션 수 + + self.logger.log(f"최대 선택 가능 옵션수 설정값 : {max_option_count}", level=logging.INFO) + for i, name in enumerate(self.option_info['original_names'].values()): + checkbox_selector = self.checkbox_selector_template.format(index=i+1) + checkbox_element = await self.page.query_selector(checkbox_selector) + + # 디버깅 로그: 현재 옵션 이름과 필터링된 옵션 이름 확인 + self.logger.log(f"옵션 이름: {name}, 필터링된 옵션에 포함 여부: {name in filtered_option_names}", level=logging.DEBUG) + + if checkbox_element: + # 필터링된 옵션에 포함되고 최대 선택 가능한 수량 내라면 그대로 유지 + if name in filtered_option_names and (max_option_count == 0 or selected_count < max_option_count): + self.logger.log(f"옵션 '{name}' 유지 (선택 상태)", level=logging.DEBUG) + self.option_info['checked_states'][name] = True + selected_count += 1 + # 필터링된 옵션에 포함되지 않거나 최대 선택 가능한 수량을 초과했을 경우 체크 해제 + else: + await checkbox_element.click() + self.logger.log(f"옵션 '{name}' 체크 해제함", level=logging.DEBUG) + self.option_info['checked_states'][name] = False + + self.logger.log(f"옵션 체크 상태 조정 완료. 최종 선택된 옵션 수: {selected_count}/{max_option_count if max_option_count > 0 else '무제한'}", level=logging.DEBUG) + except Exception as e: + self.logger.log(f"옵션 체크 상태 조정 중 오류 발생: {e}", level=logging.ERROR, exc_info=True) + + + +[기존 선택자 정보] +self.option_product_locator = //div[@id="productMainContentContainerId"]//label[contains(@class, 'ant-radio-button-wrapper-checked') and contains(., '옵션 상품등록')] + +self.is_all_option_checked_selector = //*[@id="productMainContentContainerId"]/div[1]/div[2]/div/div/div[2]/div/div[1]/div/div/div[2]/div/div/div[4]/div[2]/div[1]/label/span[1]/input +self.total_options_selector = #productMainContentContainerId label.ant-checkbox-wrapper +self.single_option_locator = //div[@id="productMainContentContainerId"]//label[contains(@class, 'ant-radio-button-wrapper-checked') and contains(., '단일 상품등록')] + +self.original_name_selector_template =div#productMainContentContainerId li:nth-child({index}) > div > div:nth-child(1) > div > div:nth-child(3) > div:nth-child(3) > span +self.edit_field_selector_template = div#productMainContentContainerId li:nth-child({index}) > div > div:nth-child(1) > div > div:nth-child(3) > div:nth-child(2) > div:nth-child(1) > span > input +self.checkbox_selector_template = #productMainContentContainerId li:nth-child({index}) input[type="checkbox"] +self.image_selector_template = div#productMainContentContainerId li:nth-child({index}) > div > div:nth-child(1) > div > div:nth-child(2) > div > img +self.price_selector_template = //*[@id="productMainContentContainerId"]/div[1]/div[2]/div/div/div[2]/div/div[1]/div/div/div[2]/div/div/div[5]/div[1]/div/div/ul/li[{index}]/div/div[1]/div/div[3]/div[1]/div[2]/button/span/sup + + + + +[새로운 선택자 정보] +옵션타입1의 옵션 체크박스들의 셀럭터 +div#productMainContentContainerId li[aria-describedby='DndDescribedBy-0'] input.ant-checkbox-input[type='checkbox'] + +옵션타입2의 옵션 체크박스들의 셀럭터 +div#productMainContentContainerId li[aria-describedby='DndDescribedBy-1'] input.ant-checkbox-input[type='checkbox'] + +옵션타입3의 옵션 체크박스들의 셀럭터 +div#productMainContentContainerId li[aria-describedby='DndDescribedBy-2'] input.ant-checkbox-input[type='checkbox'] + + +옵션타입1의 옵션1의 요소(체크상태) +
  • 썸네일로 복사
    삭제
    편집
    옵션명
    16/25
    8代太空舱白【强力全壁刷】【强力去污】强力电机
  • + +옵션타입1의 옵션1의 요소(체크 해제상태) +
  • 썸네일로 복사
    삭제
    편집
    제외된 옵션
    옵션명
    16/25
    8代太空舱白【强力全壁刷】【强力去污】强力电机
  • + + +옵션타입 내부의 각 옵션이 체크되었을때 요소 상태 + +옵션타입 내부의 각 옵션이 체크되지 않았을때 요소 상태 + + + + + +옵션타입1의 전체체크박스의 일부선택상태 + + +옵션타입1의 전체체크박스의 전체선택상태 + + +옵션타입1의 전체체크박스의 선택해제상태 + + + + + + + +옵션타입의 설정버튼들이 포함된 박스요소 +
    + + + +각종 옵션버튼들 + +한개만 있는 버튼들 +1. AI 옵션명 다듬기 버튼 : div#productMainContentContainerId button[type='button'] 중 span text가 'AI 옵션명 다듬기' + + +한개 이상일 수도 있는 버튼들 - 첫번째 버튼은 옵션타입1의 버튼이고, 두번째 버튼이 있다면 옵션타입2의 버튼, 세번째 버튼이 있다면 옵션타입3의 버튼 + +1. A-Z 버튼 : div#productMainContentContainerId button[type='button'] 중 span text가 'A-Z' +2. 1-99 버튼 : div#productMainContentContainerId button[type='button'] 중 span text가 '1-99' +3. 빈칸 제거 버튼 : div#productMainContentContainerId button[type='button'] 중 span text가 '빈칸 제거' +4. 한자 제거 버튼 : div#productMainContentContainerId button[type='button'] 중 span text가 '한자 제거' + +5. 원본 옵션명 버튼 : div#productMainContentContainerId button[type='button'] 중 span text가 '원본 옵션명' +6. 옵션명 되돌리기 버튼 : div#productMainContentContainerId button[type='button'] 중 span text가 '옵션명 되돌리기' +7. 옵션명 모두 지우기 버튼 : div#productMainContentContainerId button[type='button'] 중 span text가 '옵션명 모두 지우기' + +8. 글자 변경 버튼 : div#productMainContentContainerId button[type='button'] 중 span text가 '글자 변경' +9. 변경하기 버튼 : div#productMainContentContainerId button[type='button'] 중 span text가 '변경하기' +10. 가격 높은 순 버튼 : div#productMainContentContainerId button[type='button'] 중 span text가 '가격 높은 순' +11. 가격 낮은 순 버튼 : div#productMainContentContainerId button[type='button'] 중 span text가 '가격 낮은 순' +12. 옵션 타입 삭제 버튼 : div#productMainContentContainerId button[type='button'] 중 span text가 '옵션 타입 삭제' \ No newline at end of file diff --git a/src/error_screenshots/20250616/screenshot_error_1750078031.png b/src/error_screenshots/20250616/screenshot_error_1750078031.png deleted file mode 100644 index e888a1c0..00000000 Binary files a/src/error_screenshots/20250616/screenshot_error_1750078031.png and /dev/null differ diff --git a/src/error_screenshots/20250630/screenshot_login_failed_직원_20250630_230827.png b/src/error_screenshots/20250630/screenshot_login_failed_직원_20250630_230827.png new file mode 100644 index 00000000..87a28f6e Binary files /dev/null and b/src/error_screenshots/20250630/screenshot_login_failed_직원_20250630_230827.png differ diff --git a/src/error_screenshots/20250630/screenshot_start_Percenty_task_L2458_20250630_231112.png b/src/error_screenshots/20250630/screenshot_start_Percenty_task_L2458_20250630_231112.png new file mode 100644 index 00000000..1a022611 Binary files /dev/null and b/src/error_screenshots/20250630/screenshot_start_Percenty_task_L2458_20250630_231112.png differ diff --git a/src/error_screenshots/20250630/screenshot_start_browser_async_L601_20250630_230856.png b/src/error_screenshots/20250630/screenshot_start_browser_async_L601_20250630_230856.png new file mode 100644 index 00000000..167c9dde Binary files /dev/null and b/src/error_screenshots/20250630/screenshot_start_browser_async_L601_20250630_230856.png differ diff --git a/src/titleManager/gpt_client.py b/src/titleManager/gpt_client.py index 03eefa01..eca7db10 100644 --- a/src/titleManager/gpt_client.py +++ b/src/titleManager/gpt_client.py @@ -180,7 +180,7 @@ class GPTClient: }} ### 참고: - - 태그는 **연관 검색어, 속성, 상위 분류어** 중심. 네이버는 20개, 쿠팡은 6~7개 추천 contentReference[oaicite:1]{index=1}contentReference[oaicite:2]{index=2}. + - 태그는 **연관 검색어, 속성, 상위 분류어** 중심. 네이버는 20개, 쿠팡은 6~7개 추천. """ response = self.ask(prompt) diff --git a/updateManager/updateLog.md b/updateManager/updateLog.md index 442d3471..5ca4a4f7 100644 --- a/updateManager/updateLog.md +++ b/updateManager/updateLog.md @@ -1,4 +1,4 @@ -# 3.8.24 업데이트 로그 +# 3.8.25 업데이트 로그 ### 오류수정 - 내부크롬변경 @@ -11,7 +11,7 @@ - 설치시 관리자 권한 필요 - 이미지프로세서의 처리방식 개선 - 번역시 번역대기시간 설정기능 -- 번역모델 등급별 설정 기능 +- 번역모델 등급별 설정 기능(롤백) - 상품명 AI 생성시 네이버스마트스토어 SEO 적용 - 확장환영메세지 처리 - 방어코드 포함 diff --git a/user_data/user_data_909d2ef8-7053-4006-ab40-49eb49f20383.db b/user_data/user_data_909d2ef8-7053-4006-ab40-49eb49f20383.db index 7cce99e7..231b7774 100644 Binary files a/user_data/user_data_909d2ef8-7053-4006-ab40-49eb49f20383.db and b/user_data/user_data_909d2ef8-7053-4006-ab40-49eb49f20383.db differ