브라우저 제어 및 옵션 처리 로직 개선: 불필요한 코드 및 주석을 정리하고, 옵션 핸들러 초기화 시 인자 정리를 통해 가독성을 향상시킴. 환영 다이얼로그 처리 로직을 비동기 방식으로 변경하여 안정성을 높임. 로그 기록을 강화하여 디버깅을 용이하게 함.

This commit is contained in:
9700X_PC 2025-06-30 23:32:38 +09:00
parent 33b6e5fbd5
commit 4d7292561d
15 changed files with 799 additions and 243 deletions

View File

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

View File

@ -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):

View File

@ -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,
"메모 입력 옵션을 설정합니다.<br/>"
"메모순서 : 기존 사용자메모가 있을경우 어떤 메모를 먼저 사용할지 선택합니다.<br/>"
@ -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,
"대체번역시 번역사이 시간간격을 설정합니다.<br/>"
"번역간격<br/>"
"번역간격 : 번역사이 시간간격을 설정합니다.<br/>"
"최소 2초, 최대 10초까지 설정 가능합니다. 기본값은 3초입니다.<br/>"
"설정된 시간의 50% ~ 200% 사이의 랜덤시간으로 휴식합니다.<br/>"
"설정된 시간이 짧으면 서버에 많은 부담을 주어 매우매우 좋지않은 일이 일어날수 있습니다.<br/>"
"번역완료대기<br/>"
"대체번역시 번역이 완료될때까지 대기 시간을 설정합니다.<br/>"
"최소 5초, 최대 60초까지 설정 가능합니다. 기본값은 20초입니다.<br/>"
"설정된 시간이 길어도 번역이 끝나면 완료처리됩니다.<br/>"
"그러나 서버에 부하가 몰리거나 번역속도가 느릴 경우 전체 번역 시간이 증가할 수 있습니다.<br/>"
)
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,
"대체번역시 번역이 완료될때까지 대기 시간을 설정합니다.<br/>"
"최소 5초, 최대 60초까지 설정 가능합니다. 기본값은 20초입니다.<br/>"
"설정된 시간이 길어도 번역이 끝나면 완료처리됩니다.<br/>"
"그러나 서버에 부하가 몰리거나 번역속도가 느릴 경우 전체 번역 시간이 증가할 수 있습니다.<br/>"
# self.watingTime_widget.enterEvent = lambda e: self.show_manual_html(
# self.global_manual_group,
# "번역시 대기기시간",
# self.global_manual_label,
# "대체번역시 번역이 완료될때까지 대기 시간을 설정합니다.<br/>"
# "최소 5초, 최대 60초까지 설정 가능합니다. 기본값은 20초입니다.<br/>"
# "설정된 시간이 길어도 번역이 끝나면 완료처리됩니다.<br/>"
# "그러나 서버에 부하가 몰리거나 번역속도가 느릴 경우 전체 번역 시간이 증가할 수 있습니다.<br/>"
)
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,
# "대체번역시 번역이 완료될때까지 대기 시간을 설정합니다.<br/>"
# "최소 5초, 최대 60초까지 설정 가능합니다. 기본값은 20초입니다.<br/>"
# "설정된 시간이 길어도 번역이 끝나면 완료처리됩니다.<br/>"
# "그러나 서버에 부하가 몰리거나 번역속도가 느릴 경우 전체 번역 시간이 증가할 수 있습니다.<br/>"
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):
<p>이를 통해 마켓의 로직에 맞춘 최적의 상품명을 AI가 만들어 줍니다.</p>
<p>수집된 상품명의 고정을 지원하여 소싱키워드를 보존할 있습니다.</p>
<p>상품정보수집 결과를 사용하지 않을 경우, 소싱된 상품명을 AI를 이용해 가공합니다.</p>
<p>상품명 가공 네이버 SEO 최적화를 지원합니다.</p>
"""
)
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)

View File

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

View File

@ -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):

View File

@ -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

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Binary file not shown.

Before

Width:  |  Height:  |  Size: 114 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 146 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 148 KiB

View File

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

View File

@ -1,4 +1,4 @@
# 3.8.24 업데이트 로그
# 3.8.25 업데이트 로그
### 오류수정
- 내부크롬변경
@ -11,7 +11,7 @@
- 설치시 관리자 권한 필요
- 이미지프로세서의 처리방식 개선
- 번역시 번역대기시간 설정기능
- 번역모델 등급별 설정 기능
- 번역모델 등급별 설정 기능(롤백)
- 상품명 AI 생성시 네이버스마트스토어 SEO 적용
- 확장환영메세지 처리
- 방어코드 포함