From 4d7292561d204f6355c970b8e3619b25b8435e9d Mon Sep 17 00:00:00 2001
From: 9700X_PC <9700X_PC@gmail.com>
Date: Mon, 30 Jun 2025 23:32:38 +0900
Subject: [PATCH] =?UTF-8?q?=EB=B8=8C=EB=9D=BC=EC=9A=B0=EC=A0=80=20?=
=?UTF-8?q?=EC=A0=9C=EC=96=B4=20=EB=B0=8F=20=EC=98=B5=EC=85=98=20=EC=B2=98?=
=?UTF-8?q?=EB=A6=AC=20=EB=A1=9C=EC=A7=81=20=EA=B0=9C=EC=84=A0:=20?=
=?UTF-8?q?=EB=B6=88=ED=95=84=EC=9A=94=ED=95=9C=20=EC=BD=94=EB=93=9C=20?=
=?UTF-8?q?=EB=B0=8F=20=EC=A3=BC=EC=84=9D=EC=9D=84=20=EC=A0=95=EB=A6=AC?=
=?UTF-8?q?=ED=95=98=EA=B3=A0,=20=EC=98=B5=EC=85=98=20=ED=95=B8=EB=93=A4?=
=?UTF-8?q?=EB=9F=AC=20=EC=B4=88=EA=B8=B0=ED=99=94=20=EC=8B=9C=20=EC=9D=B8?=
=?UTF-8?q?=EC=9E=90=20=EC=A0=95=EB=A6=AC=EB=A5=BC=20=ED=86=B5=ED=95=B4=20?=
=?UTF-8?q?=EA=B0=80=EB=8F=85=EC=84=B1=EC=9D=84=20=ED=96=A5=EC=83=81?=
=?UTF-8?q?=EC=8B=9C=ED=82=B4.=20=ED=99=98=EC=98=81=20=EB=8B=A4=EC=9D=B4?=
=?UTF-8?q?=EC=96=BC=EB=A1=9C=EA=B7=B8=20=EC=B2=98=EB=A6=AC=20=EB=A1=9C?=
=?UTF-8?q?=EC=A7=81=EC=9D=84=20=EB=B9=84=EB=8F=99=EA=B8=B0=20=EB=B0=A9?=
=?UTF-8?q?=EC=8B=9D=EC=9C=BC=EB=A1=9C=20=EB=B3=80=EA=B2=BD=ED=95=98?=
=?UTF-8?q?=EC=97=AC=20=EC=95=88=EC=A0=95=EC=84=B1=EC=9D=84=20=EB=86=92?=
=?UTF-8?q?=EC=9E=84.=20=EB=A1=9C=EA=B7=B8=20=EA=B8=B0=EB=A1=9D=EC=9D=84?=
=?UTF-8?q?=20=EA=B0=95=ED=99=94=ED=95=98=EC=97=AC=20=EB=94=94=EB=B2=84?=
=?UTF-8?q?=EA=B9=85=EC=9D=84=20=EC=9A=A9=EC=9D=B4=ED=95=98=EA=B2=8C=20?=
=?UTF-8?q?=ED=95=A8.?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
browser_control.py | 60 +--
login_dialog.py | 5 +-
mainUI_SP.py | 200 +++++---
src/contents/details.py | 4 +-
src/contents/option.py | 267 +++++------
src/contents/tags.py | 4 +-
src/contents/옵션.md | 47 ++
src/contents/옵션수정.md | 449 ++++++++++++++++++
.../20250616/screenshot_error_1750078031.png | Bin 116478 -> 0 bytes
...shot_login_failed_직원_20250630_230827.png | Bin 0 -> 24821 bytes
...rt_Percenty_task_L2458_20250630_231112.png | Bin 0 -> 149035 bytes
...art_browser_async_L601_20250630_230856.png | Bin 0 -> 151622 bytes
src/titleManager/gpt_client.py | 2 +-
updateManager/updateLog.md | 4 +-
...ta_909d2ef8-7053-4006-ab40-49eb49f20383.db | Bin 3596288 -> 3596288 bytes
15 files changed, 799 insertions(+), 243 deletions(-)
create mode 100644 src/contents/옵션수정.md
delete mode 100644 src/error_screenshots/20250616/screenshot_error_1750078031.png
create mode 100644 src/error_screenshots/20250630/screenshot_login_failed_직원_20250630_230827.png
create mode 100644 src/error_screenshots/20250630/screenshot_start_Percenty_task_L2458_20250630_231112.png
create mode 100644 src/error_screenshots/20250630/screenshot_start_browser_async_L601_20250630_230856.png
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의 요소(체크상태) +



#w&O)RIq_6fwxCi;dM7~&4RKHTD5JJH=s&z zjskgd!x1`bSf?)cyU+h2EZg%QMQduAL`;c-t&H9cynQf<0~Pt@%aFJQjF=0KTN(Xe*5}k?{GS7vL=?ne{-r%(Dy!7*Ylu3s^mP6;ueRGTRCi}1%oC2tglzYA*B%y zL^MEO2K^;;ne9Z1;8KC@ D&oIU@b(t zp}6kgv;Z}SoYsOEH-$3@7NC>0+(Pg3-BzT2i*$N=o^ohf!@wUE5Y~{dV0NVm061vb z$o`_;82V3U@RbS*)`Z}dl8@K8fo0esO#y09>HJ3^@w!;J2a`4z^k$}}YIAa90I??t zR^ILO|HLezHY;jHrw7e8}{*6E%rHp{c03jZ|PhLjCpNKpL )|J7HyC2 z%s{$iwp9f5u9lmTDQlI}!&eZkoI4DY>BHt#0-GrZ?m}_5%kK&heX{00)2q-Tsf)5^ zYJr6WUsrw}0RaJ ;ZF3p>FlaGkM)Ity-X&;2>$$KV)TLfyZOx zYW)ye)m|O=F1ijf |vr4-zXg6Vrl2C7l+C-(#?yIhQ(eUKGf~53srOq36T&C|3{*)JE7}dHJ%xhy@Kg zj_YZKjY{;yqKe7HE%ej^F(5)M4sJZE^oiHKo@%pr1J^g(*} z`L&L5h$ai3QmTguW|yzfKb=}^hDw`$Ew7dvGO3Jpz#$nyt)9Z#yw*YU@M0EroJI2_ zFu^Z{I02lNU$-jPsig~-Ai;YJ4vKX7z7{PL*cij&JQ59 p9MJl|`HWz< dY=5G ea!@rU9Y;!m_CMzoojDEYWuB-Qk%t4e3YrTaP0LJgk{gikwv4pj{mz9n3 z>nW*fx%nTS7#3^LCHee0Xq#UPzdlSnq9ASRj-}w8>y+$*d(-vsNbuyLxX_I$jhS;icEP6$aSJG` z<`0RmioZid!IT_!fY_k@&+e86T`?x4E9jAoPCIM12GiDWhq5RQrt+kwBN;&!a{O;F zd;hv~_*HzZyTD+itE5w~39Hf2>CW2b%N> tUotSBv}Yr%E6l zL#2%3|8`hO7mcsXNL@#B`GxtiRu9PWR~*1!xi~t)ukw)F1mh9(TQE&A@T_6Zu4;em zJ*AtD&|b>K&!bxdaY+%8GZs_(y5i`t=;BX3JM1V8hVI5PG=80k0qv%`)BrP8s?V$x zc<@2K$h@aDiNpw?VTeP(8;*|Zx0o(x;*r&FhAe@VH3bFoxx?^b&a4U^tKqG~_wc_l zTZzylu0G^D%^wk(;gXmgo_su;g>$G{3fy3)e3VpLKa8SAoBz5gmIwsv?|PqP!ORKo zZtv(SMvTmItLJj{dWGBcUgg$9|LK@xu44RygRl3r^U3Qh-! U zW>WgVn+nt;-}G1}D4AnOg+0gi{$Tyx2~jw7T+rkU{~VySki2K-a^ibD zCa<;1B-hEXl0a;;u&Rm-0iu*0Z5#t;oS+m-3?vnBB^9vXfuP?S8-`-zgk2=98_hW3 zQ+pIfd#a1rDZvhNCgU04>W|y(M`@N?S(Ow{rTbBa6KHLV-hz22)uy1TDqcPdeBin8 zD|5608x3HtP&k 81*nnJzS0dTr3c6A{$n*BnScNSi_g$><){LwuB%HhL=T>+-eqj` zHw7%1VpFK{{%@=NTdGAT<*gMj^;-@IC7o3s>0}FQWH6qp8*@aqSV*3NmWGc29dW@1 z>4}Z@PXW&Za6|cFrJD8LwP33smsbIT&t-Gs5T (>0lphx0+af@9WRu0 zlgHAzs*D-RHxQ_P4OZ^VFwPp&$y@=W0Rdt(T&-bFnaheSF`yp|n%=<)RD2)^PONeA z@Z{tx0MwoUk>I|-QU~~%we~>S#-?Dkl1u2^q>!rQP);G{zBWsFCNR0c%MgdM+%v*N zx@20fqs_|qz3=zsT^e*2axH?%NhdqIl{Mv*av*mIgVE6pz;h!EX5W%=Uu%4SpN_u? zQ7bWRQBx=RW>X>FmI+#OzzQJk-O$pn{sdhUV9=N&kR;His{+4&A?oi87>YY z*~5BO#{9y< l&iy;o - z4%`kn$zMA<(meuJ;2$GjAuyeSl-r%G^_(HSJ{O0h;MyI$A8q>^9u$W3Ku8CI9$JN% z&N3z+aAP<-mrKw-WMrJEa(WchbOR5s0IbKkKU1VL2Ecy)&%9-9*Hb4ci!yikD6qqV z!`EgFN184In(QBdQ?v}-URHbtx=20?4izi`I5YADN?*;GSm4k=c$162L$PBpTQw7< z*dM1wZax6`Tv-VPT3~tAm_xvpWLcJT?T1A8ewJAwvn6HW30MFAZH98~v|e`)4 h)%W>5&!3OK?wiv&*SW6Gc)wrkQ+m`Ozt--uw>P=eW%`4Kcucun-!a%- z|Ne1e4lIJC0;)GKpfMbbel_mEt 0=G91g`RLOg<{5p2fqAf~Mg6dZYj{j EQ3DqN2^L 566_0FNMNPKUo@p#7 zlYg x~i%fei7};Oi=Ax)`THuY*b#KOu?)BCH zjrc^Sx(4Pn!@60g_nVn?(sW R{3YpVjtCI=jCoS558@R ziW2_qGrtly-&gJlR1f~sL*b8B$?yX)3tX&XX#%u-?wkjNv6X+mZm+)xu413tpa}XR z@1M+m=p=}zD)jDWHTt;;MG2rEsQk|av#acD3(5nTP4FaOJg&ZQqJD50jXwNyQNow2 zf%<-xbyifYA;8a24l{4b)shh(Pq53^+Jx%H_ZVM)k68+u!Bf(?R>wB7Nr|t+>6(KB z(U4ub6e~MJ> 3F>;cwbT-5`%F!>jzGYp0r!!;v0`Maa %&ho_$u4YVoo%NEwh4oMAxh73_l!B~sEW;ropuo9b8; z#g5BckLWAA75(&fQBLI)LVjOW$Ra5`SW8|iKk49&)V&rn!MpYN&ZkW18@=ryy}=5V zk Y} eF?Tbs0aIPQtf+RUxup3_wN{Ik;o^;(071iz`_w4 z5BInc5PzoY|Lr+9B7>bWQ7I5IF^|RO6K=;x1!ZK&@3(Lompa=hDpEdXmIBzXuKpR1 z^X0N&U?QQ@fJiD8(ukClho$mDWi$D_ocW2oqrS8bC1uIU59-B{qNW}#W&_V)P@eC` z+6a{BpgcPLq=DWAG*}w!l%r}{gg=1)&IR1<+T9v2AZ%FeG_UV{%BL-PE+>K~9qZ(- zAAC^;VLW8-`)0x@1L{?ezWoNclI6Iqj)#Y&pzl Y0Qm&IdX-j{>Yny~d37rXxXi9n{*7p{SI zyVk77=>rmb?TUZ>axELmiQj$Eh{A$qOY}q%#e%oXZfYYWGs>sdhE|LmZKfRjn%lz> z$rI<{^ege%+8)nuzXA*PJboQqu{dxOVJ_}F&y2)mWNfUo`0iwvxS+4Eyzi}tW5vo3 zkORYQ#bCt$i**}TDGFp|Wxf0U%j?TEysQGCdRz!c9vWtEOo+pAxAt2Eor* K La<80!+32& hfukQyycW}*IqJ$S#tfFNU;Ayvv2s``o3SXobR3RZV7Zl(DKW!7YqMBa zWL~NM1P~aUOr4vNL8w&_nlvE){d z4NUN;tbVyi=TN@f-2!6yRk(V4ediDW8^-Ss_av=iD_zo kaKv=D2>746DN9Xi_WY(EgjL0jE^5l z{$QFEzeQqo;uxzUOOwF+))VF{6|2Qc4NSDfv{1I_1&^uAEO#EYXQH99wY5cK0*f+P zp}43>26qKMiq4I>qT*t4F)>C)Mr+?fg7UDKdAImE@H;Qe;r*y7U3xC13NEaX5w3&X z?6=#9_tlaCLP}%R_!Qj5>|xaiD_|9Iv9*iux@>2sHeyjy!d#BhC?-x%M+a{vg+Zyf z6mvl&hnhs9G$^Sj(TR@hzELeo-SGYrarm(JU$c*L>zWH2JKwJF&c6|HP_8jYnxmyd zL(aQ?@6}JZ5gNX$9oPepV)+bZk%+Jfg(H}laa%aOLPd(Nwr~X2g*|z~kYACJF<-Hk z)AYUcXZ3cF@atH=VWHH?RAo0ueYwifnUZp7irf)#A!Ooai%hDn5=4Ni=g%*#cwu)M zk9CW4{Vz`3p0&;bo&$n-L=K_AquhW*iP+D`ulyOe %$qwRSuyZhiO$ucU*K`Ht2<(=`B zvbj=FS!$K)3JQ1hdS|jrhL7;^wdwEK{?gkCsRa&(hW!vUmRWw>MRtQ2Vj?p&X9$ES zd`h_SB?H+T`z3;7pw9t`?kyWH(1Z+>uvh#!Z%%1G1-Dolh_Uo-*+UK>Z|J)v+_61? z<1uBDaxadEuSN&6pF9RmKX`c4rEf~PF l-Yb`iz^v_)Gs z-9a6C)UeQhN}@IDEIm~W1U$)D?g%NwZ1F&%avWH{7U{*Hj>*mlV~@#X)Fx*>*uvQZ z5MF?n_xbT_%tINd{SC@o(IKouIjioMmYYl0$5dnx_6lvx2_c~n^JVCe+Foq;0J*yg zVnbt^KDS3TY3@Kj^fE|?$uYgrp?2aihfHZf2tA2JO(F7kljJYesYfbTrzp*?zP9?l zt|Tg&yI$GfHOjAXPFBi`+s4+`<5%BRNQbX9M 9S ^5#jj6 f z%G S$kxV9@4ZuhwTwE-ZXH_C_g?`SCufi0-xkooP+M z@1bYUhrJ?dJ9Nol6kdO{T&)xGfgQgDEeE*Ahn&Bk>6^I2eMa~ofA7T9$2gyVGZq}` zb~wNs^o;WHP@-LXlt3ikgy4=Qy}daZxCRj}hu2Sqf<7 ud9Llf1uf zN#pr`LYu51Ir$1UX6$L#o1>322U=m*;Vz*TcNn|~MiJk&z4bd2zuu>q6B;q!@Ngj! zCajLD!;@%%&}U?Chb}wz=I4{01_~PptdO(}`;8%uG0 -M8>xGaxM z=4_k)@c0c+#U+ZWRvlB*4HTPLUV_dYhFX0BYj1L%l6vamJv!MB&CRhQ7SzwmL#_J8 z7XdC!c)z?Oe|zi=)Hup5(E#!_(7;1FTy$L$eJym&-y5GP2eZHI+xC!-a~%z}pB=AC zI|hHQY1p%8MJS rjGBsy;@=n!wDxS<)55 z&a3Q4?vb?x$+ALnX8W()>Yi^=*XLgIOZA5&^1_WTJ^te6gmm=Nr%SDhbjQWS?)Ma{ zsj5Qm8RzpxVDpV7*d$lL_sb*~AhN~1GfmTwK8=Cuy|oxY(m3DUqvQl}gxp$R3yies z@;ZaiCKwtUOT)-6_yW_b8)_$TvbVZ)_1nTgY(hagrC5vn347MV UuR{_fY(NbGk4c(te&SL(lX9 K zSsC^RYN`q}3Q&hnD_`qy+P-buuHC!Ar89aPrbBCwkMu4PSNoAJbTxUtIVH{yb6Rsi zBDf4Ia;jQP1hjJP+^?~L%X9r)>d?5hmkR5Q%ZEJ*egj3<^s&oBn+K8=v1S j+=@kK(pg$%1lvg|TvXsju?WW^rb z`IQOuTwUqlzMabE=3h}TU7I!LYWfd57jy(6>?d~}v_I^MN*hJy6?_`J)i#e3d9hE& z@&@!P3_1KpS#m1B3Q=>BhqNWef<)6c`o k21;7Y^1k$!nLeMH5?;> zt1Fu%(PS)qz~XbNvgxFA^J2k| 6NvYS zr-Q@XJlX~IX}hTAAEBS^ oaqdNiTtPcUt8D2ap= zJYxO1wlFdxX0P1g4dA{Pbuzf)i1hTqLx&)&6B8G=RV*tvaRdyu9++Tg0PR1Bw2RQ6 zK@aioT=2cJ6HRwNx2#1jyO@a)* z0a6E5v{0=N4Z T?HR}@{YH+4 zB%)6x9=VlOG8c_&nKxh#_ 48adnfu{W|^Si;e7lI8m zNFE 8L~5tho2(RBS_7*&np{ zu{Sv#NF0;>@52G^hH?~4lW)M-x(XOn41J;so _@J85gr`mKTX(50=r#1Ls z(|=G5L`Oy{kG(K;|NcyB?#)=m+8kn+ZfI(@VI>oysRrr$kp-4y>?qTUzchrF6Oa?( zUtUZ8iZ%0MvEAMwWoUUN$x9u?ws+ wx_6Jm?BZSlXV=@6Z5Vb$#8PO@9zuzD)g!sNPorjV{4ojvX zyV8LAb7Ag!Il&1A%vxe4j6h(bRU>2Nh^e7rDT049cqgw*U;&ff_V5HmuIP7+9LtZ3 zifV_wQs!{~mXVn=WxaHc5kh6JwP%ix{{CMrz-$SG`h01!qxW~?WeiH1gAOMeochfy z$}woYm7PzE8K3kznXI508;GFDAry}64YR#@V-N|HevdF>v4G^u1PB0dJfLYsyVC|{ z*C&!fe3mm!U&`+zRXHV b<>sG{M*N&iTgryinIwQ4 z%7L3U{NOYQyH?@lLIx(h*>bnh!af~QY!BXlv*dDkRPJ)!*AbXj1+O4RBmK^vVe4IC zNEc=&(fKGU74VVQ-744i$;3&zOyTK|UDNu|z;rZ1u6P{+hj*wc@0O;Vu Va*+W0 eooE_cm>DjYdUx6vVM9YBFXdO!r0f{^h{1g(h-LKm8UiYblI!pU zI!++iA{=-1_>6`+KaFfe+Af=qb{6v5-jYoTu(ZNCY$*KDfWuuue|XS$3~s8!7hk z=)H90`oky1dD_~aWdvs<`RaFGWeHGX53fDV*t9I7mNr2Cb)CH@v+Ed;%n{QAXI4mS zG67@mJ_!d?T~>cHH*p#{m!IhFu{@@EJNo%^2{dhTfK+gcU_LyyO@A60uBTpc=*M2D5JJf z-^2T23@&Z(zq9wLtb|< V zSV>=*n^5dBCMEiZ)0eNpqOp08F``nSz_Rw%WJIlm|2m`=&Db<4ANDHrAb~EV90)iT ztA!p8MW+p|aI@o{IEE`lhC*+@h4CuyQEIb1hVQJYG=VKSIE6EuXOeT{kMhl%i@+CK z$=gZSrB5=G+ LvS{{(6|xWZ+f+I7M>Anmdw_P` zuw7TK4b3klHD+dHK#+YL685qoqTA~3?rx`z2ipluPy!CD@bGXXru7dDfc}d|)rN`} z6A?Xw6~wb)Jmr7B27)q-!&YKpF@{~n_E;3`6))7s;ltdRu3YDjI3l&L=qJ&lL)moY z^9uu0(+xnt qy-;{2XQ%$`0~i;lYq~P*fqT>B27GOu)gVKx&Vp-z+-!M;GM}l zf9|_5JY$7 d%8ovZF}x*i_Tn*m zQE$=ui-;qd@Uv{#g}Ypzpnv{&IQ}o7M|<%BLJ)o}w>-k5xX6V!a|1;e3(Nx_&fg-D zuCq^+6zY;#zWnvsfTo#!CvxD=;Nd{A3UJouSd^FS2$nYxx5=BU(Ac;G_Xp6u&nh67 z;lP>JT0^n>x#{VXaFZG4TW(d7IvnfVj)!yBTN}fW=p~VGosmJ{te9Q9IY>~5H6RS? zRRfY@k&5kTZ@ TR~eiAjkHTvM)J84bEnCNH>7!`$i0Dv3IxxQ*~(kDGiJvn 0A_Q#Jael=~q zy%I1>*=K^+#Piz#3t{x`mdI4-vcE+8`9X)@_`bIW>`UOS35bAnZ3T+~L65)@p0 Ll$*>0daT~<%BaN0&tC>`83M%ziK`@1L%aoraD!8Me+U(+{8dA43%t+R z;PTSn5nVNAhmk-89usI*so2k0uTneu`uj7op_gAKAbQt6D@X~0%Yl9#2Q~{j0VKiE zGTh0u-G>eye9#_uGFVkbB^|7Yl^=r ))tN`*!ov2ego*Px`+@;>pDpS+3K~9|TfjVk&-*!O zh?rA{DzdAUM$XaSN1G1k&v`gike`$ococBQc?}ImYwPP}Lpm-h0|DbJ1hh>cfhQd2 z7PM|EHbA_-bnxra#D*bGEqZ^YfC#E)VrsLe(bZ*RWkqEZ?{fc4)+slHJ?@ioy)WPe zY_*6Of>;VfII7KrQQx~QWpL4Oi)y3gTHq<%!tzA8DxuYryi13QpM@5NesO;Oz^_$U zkRcNGkd6a>#rF7&H|Z|{7o?=jVXDLQc8Qe_`dLFm!;EJdF_JDB5WK!M&a(rVFdmGp ztCGij`^n(4Z(*uXT8sdjKmm!~>|2D9!`6@GLH^_Jv%6Ci?rGkw^5JRv50z|EvPd`v zrTsv$=1C8G1k66;c| $s^k-NaV8---qMA zs`B4;fw&u`l{O$G?D+%bo^HV9&`@nw2*s6ud4i>I38)9b{Gyl^gF-sAc=IL-@90Dv zcY}i&9*%R?vyl9z(;lE7!3$-vHj{le{s%*(F_Gu|+pL}68v9hnm%e*v;)urn7V>J$ zple2v*3tf41r9zg`d(0Lps@-c;Hi{c6zCjb8_{EMDP>w-AhMIN^D-(B)$D(b{BV?B zjdjucWBMODbpn(JnwfW4Hjeu$)_h1^Cw{2IKaP+k)#a!%kELFH4%q8`tZR?}0ZGMZ z4oBXW@87-=SY9mi^B||=BofPqQw;_-kq1n2OXT_~Np02}B5P)rhPVyr(1zgyFWr~I z8z9h>k%2K2a_;r6xN$^4Ib!ic nWEMp-)@TR| zdQwGp`_(vMG=j+Y9m=!t6KKN6AoPkb5n7)c|5H4bwj @X-1K=d)_ z-ccDVb58EP`r-+UN$7&&*3uG$CbkE;1vEbTeIu|GE49BUT|)^gi9~CeC_8Sj(L25D zdNZ1tG~u)=rM(&`>@ipnX@$)R(0g732T08->|(BSg9M}~#U6~s|G{A`0LcGg+rqx7 z1`rc@bSl_W0MI6 30Tr`Y#ra+E7H?#2m=rBDygAob>>lWzUxqH!D(N|B8Blv1h^IzeRVK#M2cM> zE-OV%F6y2)@GHRg!&odG3i=36kf3hGBG&F~;Yd&jS+HkPTFRCP=10;2cp})fZ64K` zfBgKpcwlz*$X5>xVHi wQA>uu`&z?+ZYTyJnPs`fl*?V55P-egp4kbpV$#nH+{(?G;b+fK;cIW6V zOg{c?WMZOjdM?r=Af{oAfr>l$#PC(4<*ea9v};WZOC}E`yq=Mw&4hbL)JT79=!9P) z@GJo`3)95iq*WMnE z} zQ%X#v+Z0N#{>m$9E-Ae(e05G&_u92 T1~o16qaY@@KqF*)1+XYqVBC{`$1en@y198J@x3=by2tww`Bnf0 z>Gt)P`t^o~Uv~?hkF?TpiO4CodCNR!tMp3ol=*FnQ}p+PkFu*B(iUb3I@WBH+uoDA zZL+5;(bW6>RgWyw?4&lUr6Z5IPm`ptpXS_gc4W^vmAY98HanG#@2I|q9Nci1xadQz z-d3`076H n;TW>Sj{EujBGW{uz{F2u>uinZdMSNjh2>mx5AK}DXWo^EBk=dy z7(c_{y`@NH#yQ)_O2byRUECd&M ;gt0EYD_cq^@)}vqxo4OO89UKSq4(W>>qr% z3I(I`vYQbX2YbWI5e|}B!XtVnYY}0kx>Xv3?LkEd$y6ZGQW`wv_yWSTyIU`{BkJkX z&hgn*&Dt+|BTQK`-9mN_4nN#Y0wb#%s0U_$MMN;$({fi2t#?6##k$xxIQSwgti41{ zibL%9@dat$zncT@a#_2*aLU*xaLOl-f@CH?npP+ftG9g5vq)awclfbHtNsWRtuRSx za*vj-Zp3$d>Xff2>tnJ@iL6)XbDyl-b;EO-fO+O!%WRyZxM{EId!fg&!=p(@ zR#Rud*&0H9{@l4lhJy?}uPV>Xu2ny8xmdftu!$sJxt;nRd5G#Zr&hwG%Gn )W z9ya^PyJo9p8k;Av^rGQM<(hC}MAZ>lrZBSIVl@=+JiYg>k}qQ8K5~SlmY0{$EB7hA z=;nQ=c(L*{qY`{V1$|k1*UpBr-(6p{wLor#*@rU4wAL-=oICtQ;+?9@RI!ds>=N UM+K$uGe40bmszDlE#a_wN-~`^{MCFQFNZ}eb&&xrv_{)-xCO}e)yo+fy-@pc* zLxLs3GeL3+!|odXv9rww?YiWpaVRAr0WwDdTSy`Ms>xE5xhCV9-P21;Uk-oOCU+`x zcW^C8YmhH#URmv`r+@hS&H jH}+ub?g6hB$4dfZ!gP~j*7h2 zTRtNF^>N(BJ9Mldnxx@hLJ){qkPPIHM4sAvvp-OgaHx(QJ9aFma(xw>2Tq9ij$(q? z6IuW&!Ta?ytL^8C<}wB4TO#VFbSfiMaYDb98P-;U=9i6kgl-FIH{}guIKbyhfjd;1 z@Ka4#vN)To1kE&Lj{6(5a=&!El|1pYIq6-Xf5C TEYrljG0vqNBr_EGk C6!X%qqGHdePB3VbdjLJBcn#8XE@zCkqcfVK~G?*Qk@CgmzHdx4&+ zppQL2c- R1s-(ljES<-44OM5KavE5p?xF;rhrVFbNgh(UAq#ePJZJNkf!Lf5?U}6bOK(+b1 zdqrBBzesm)?^#s@W|Y7{%I5Hf8zP`a{#-x`dqj^N+wTB#KZ6JtojxR3+D?ZKACAA# z$Opmun>P}$XN%|=8aA@9;p`)PN~GaV`LF6a!6Kt4BPRB6ix3w?tHuyu_JBuj`tbUW zw+=g7?xrviOgFw2%>O8h+=KE69VBYiAR=lzo;-6kC=Y+yi@+JjinkrLI*leNjC%H3 z=7PJvxUb*et^TKma~&B)do9p~ *Ec-LO(2XVzE#8b#Cd{-WFr GbPSC(oscJwuhmttDyxwUoH_l;!z^s=Oan z`Mw `O $K z_Oy@&I6m3Q2wGifR~2eT2o=T81R{`icmS 6Joxk2U@=cN0dXnC5O+_xk%;-8{UW(ufEDzh8{|RG^s>t;d0f?^h$mR$VeOGl6g^ z)juDYB}8G96oTnFIdgp_C=LOK&OtXC`W;LfEZV5FoMsjg64R}fq9V-l%D|suKk9!Q zJ3HsHA(0=a+|o7P#HiAf|4rq;65_|CD`wN&POQ>aeZvCr2UO@@s|SG8y`WsVWHY64 zHmsIq+DGC@Q+~^Mfnm1)j$jKMt~z>ssX0%YLj!y;GQ&>%?r7t*9BdOpVmJ0xQzRl* z#*nH`1ya^=P&fQ{q)NUulH#>M6AD2w8$Ep`x(j7}&4Ckp@Z!}yo6J~fYD33IYdn&^ z$tl0fICGX(5p_hqKtukO83vn~J4X{uiF Xgs-MObEZfY;2S7>?j|ETwT@o7CgOEJL&0cuj@Y0afIGFDdW z59J)Xx;TQ2gNCY fj?j^}H(zs#aDHZzb6%fciA;XZO zkhtZ|>w7PMK$Qb8!gT!l(&3PXo9$>>p{6AdC4RVmZbImfE{?35V552T^0)KF&8$y^ zX%(>cjibO34EPG -7N}|F* zfT&BZvm@sV|AMyiKT3rqPMN 25DqZ}eX$Z@E{ESU?No0)(DN z4zzRsjiwXZN3Ixkqa%?L^lei6{q>bIGQMVP{n}kbk}w|h^?Gyn{utAbt!y*<*Cj^} z;{dXR`N@__hD9x>w#}EihDZ_)e=Huz-t%WP-@`Gne>=MT1m)UW?K4{Iq8>OYd{vM{ zO=7V&5As>w&Hx^qRLdJ2ex;V;x&NJ)QUo#ndZG;q%|zbl^M~Nk3fSMW*meT<<@WV} zMCa0A*{8aUO1fP*PJG#e>{BVw Diz@I?}|GTu%8B; zPrhL`yk$~DU*F+UTXplOsyv+4tb|$68}>{u-;xGGr2d|JvNdhzjyy#YZ7Z}6$5?2| zL)#{zgA+|9pJ9b78#)@W|8o`wm3K(MTja|Hk69W-SQ&8seyCGl9mOhH*YJ8LsTMJ0 zeY3yT9X2~FJe`O<7T MQn@=;ZhWKz=8AB{<1c^uhs zN8-VY3%a^EPqZ^Rgjh5Zd>5o0iwxc4Qg0R(vnvQWZ`DemD23EfB@F#x%OctWZl*A^ z*EAtMHiHZ)ZFsaSTvQ6JQ)Z*T#A>%t2-9xG-dJ98Q-S{9Ihk AR5}cn}II*WB0^^vPF*{X3 z%1JrMlYvr^k>)l 5C-A|IAkv!+K)0YXCMD0JJD@{tA z7$RK^dj%EIL?p78^ut~qojo1#9W0 rwUpenKAvB)&1u`Nauo zG*Y5 cT8rbr?>57?QGkrqpiJrS9`-PV-kx*;){JnEG$ YUEj1Cg1c+R4 z?+J$&HKYFdCrSncn~v3u;{Yb0VDvN-8-5@1p=f`(c?zvHI6p#Z$*H6XEu8Vu$YLJg z-J+re9VL_`${|kH#s8}X&_4TH=yNpjbm-MvTh+>Uk+$4Z2>&PsAyVZCaYh!wv3S}5 z*25Bp@qz-9hWcLKj2hdc9;h-+O@&mnP-lcV$}{3j(IlC!((6+DX{w=Dw=~cr)H_OS zY#0)JM^aosT@ V*IX|c4)9>wkxOOUX$-#;%sVyzUXRigfh6Q4P zHr=`1F?n89RrTyy32-lN(Kt5&vN#yPFq=9iks|VHq9iX*gr0`w@tG+K2RpmYua`p_ z!qLLV-rH1F)v39{f4Z9Zb}#g{OIltrtF58G R6L6ZcQQ8|`|HOE9}F@4tPh&4>_ecOE|Cx9CwG zJ6AjFit{0GW<<4Z5rR(;s_M5TarAvgpPhrFV )ieq-`A6lSnAG#|=|h-&@YQh9V6o^gIsqwam=#n{ zkIrzpx|FmjZnfXM#H9FU7?OH5_J*x@zqpoRlq#BN>2}$PL;97UxdtVkUgonYn+NZB znt-I@EX88p04PKI)%XPSXP+kW`uk8&3t7ry5nmFh9PtGiMZ}qYIzOAZ3X~j>InoEf zwv&hCS#D4}lg=a2vEsAnf=4;%wIzvnCE~~<0aef_i~g)y|GH$X81?hgo0#E{7d$0X z-FpvcGcK54C5(#(nj2)|EL3QcB!+ndkfVHyD4C^OpbdtE5Ql}~V{!PJYZ(HXZ^?Ke zYWcbZmcCyF%)7$NjE}~*G_c@Mgl0rl5T1(TmXas-2kf_McJ9~l4dpu`x{db40+!v> z>+F&Epv4tT(}~jPu{Riuc>Cm6uV~rC1zs7kB+${`B>}F6&*vulp0Fyjl7Y$p)JKe( z7QF=AcOo1chiZGBc0ypuFA2-)%ZXMBi_-CsNZByuxiL^yh2p}A;LT^x5RnTPy`@1; zMh2S0Bq=Kd*O38d6aFXwY}X}#kpiJZmL38 Ugw{c>g~+Cq zX#0Coa7ij@Vi_Bl?Vd~7d{3crgp-B}^}o9M&IYG^f%xmyTM>2!78o3O3$7nPb@P=1 z*E8^n3JD4xhkQHfiDc+r3j~%$d`A>t$p#8-Z*Omi3enL|rnb-j3v(_nmx ;>C z5-|9nGH#;_?3nxuaRx*xpJmtV;J jM` z T+%Ju@5+DVL;sTm zcqr<)M3fMY7KpI3N~}5-{=7=G@{`ugC;v`n6tbFtgT%!l{m^O)z>UuA0^!)5+4q!} zYRA6GY{rQ Mas#cIyYUw3It?rY;{*9Ca?RZ1O8V^3Z38_^Njbw< z7ux8~-tg8KHhRf>?-MlcAIcZN?AF!I1{O*?(%oHrbNn-&`s!AkM@A^L8iIG&Ja2s7 z(GYF?X}8&|$rVO}yAPsHM?9-#vDnlg5!@E+R79+msl4Y9BZcMyvk9X(`7*2{X&JcS zl=z0i82X7*LDUT_W~AHiAB?X6aENJ+h>L@x-rPJfkr&oW%VRd*V|P$fGA)J^b~tHn z?#tMe5_z?h9cqW7n|~mVL01>To~6_ktk^)e5YWu(1J4Ltmp>sK!dQOLlFLhT-s-&O z^J-BgtMgXuEvMr&O`K!=_>SCs9;I@cmKK-U1s#;8=4CNf !wnQXKSjwHXeqrlCr^pa-4= zpb1|2(Q=nko1OTy5KWpJBE7`L#e*SJ7+(oJ9}XlQ2R2?XGsbQb8R-T@gy{MY7`E`_ zGfIiFJ@2Z`I#cfyWD(?aUi0F;zU6=59y_0)0Q_6aYh)mx zu>HP#If`X6!m7{m7|tPss2`;Q2w~`hc56s$vQf@lmjD1Zy>N@g0(%bz4&G~(S~cwS z9WgaxBjcj~=~139&|wT`wGus5srF})EA+jusZw}@#-b9+JadjQ)8ya&bobcJH3RuG zhlTMCwl@&yD|>@YpT<)j*f`*VVN}|xp*^*0iBgfBXj+z}k30lFkZv03;E6ymzyR*j zac08S$D;1OPZ)9#!iidqjqOMDy#Z~oH%zun=;(!hH3}X(tqHAI|DCkxvo0>jnfz1u znBj()`n{GODRKPxGuDJ_Y`foBymY*9d6=}DMU-uCL*8lrYm3M3Sn!EEZ-8BJb7H>= zYtEf@X3+<= 6VVjT;IcuezZ^W^4d78RsSo6G7# zqI)l@Z@c;Y`A}`Z!@Pt~=lAb+|MU>0{!o`DouG%q%gXDcH%B+IUxjK*o>z4zrGjz? zSNdh^Z(2A_yvz3S+C|2tRWDg3Oi1jLQUsd?&H^C}w+)zTgc@C@ubF-Wa$_?ro* z@ouwHOU0T6JNckN&zQvA#s3yHgpTXf5AKp0{9r75Xq$l4qvVspEQG>4g3Mpo8+b ztyi3({LA&*2Zs2T)Yu{3p-1aGPIlO6FxcJW@VI^XY{F-e+boo3hKYZ<=4u`#o(<<{ z(-x#pPGX^b$lLKy`Q*`!dr-t!cfC%0F?7Jh{le303{f#6;Z>ig>(kF0DOm-jXSt1^ zBVdh*n`nth9#32YOI3yH_P=9x?qRJDR@;dAVHVIRaHbOn_K }USs_f-pY6vCt``Z^Q{!h;07@{pRUibPUmJw-r5ERtV2<^Dk@B$BW{7dmI+ z=QcM5{?Wf5+O5QA6F+8W -7@xxhP>(D%fqeu z=6?q!VsDtF3h9sr`fG)QE)#ia?vBjEF4(`qkPyO+E1hr;;}NOHaUiQcxWYoV_ldmF z=`5~LDah{^TiX7&kAk%H4jtXcODH2rBr&20 z2}Qpb(vXyKq=8s17?WZp&;t$3rj(qq<_81EB=y9~AbzEO@m )bV zyZ7Heq$M78K9RREpPiOkN%K|q#28js^StZqsTsCs)%T?;s_N)Mr6W6xWg_n)?$f@f z;tgNyiLt^$jFt7##TAKQqS<*8^G~{pFJzu4T+b_cJnB3O-+RcSLZynVogs%*`e1V< z`$atP8*i;&V& 0rw1iTtV?{t-p@AK>UE14KeuapzqXqU7%=O#;_5nNTQm zp@ugYVJ)3!b9*v*1DdZSIhd$~@v pEdPQ#F8~8BMUe z8$uX`o19G-UfuRHR@+$36FhK6hV30rrAjna xQkD5CR;K({HA}Gj6b?2@|{}G;E zW-0}_2T8IO$?7`U+L2=t-dlvSRpu{+@d(aFttL)RJayWK_FW`KGL6qTb;e3CtcO!y zSN9(|y; Q+a7Hi` zCPd4kVQGH1_7NH)POI yG@MY22mErKOc`)zIGAmJ(-awr+N zQlObH_L%?7f1<>SnHsX`OU>gT5%FW$j=|I<;RL>bFhn#LMW%5+yNf+F&HpL$yk-yY zLCTI$Gr|4I8Tw+?#YvPllZ|ks4}-1w&ZeD{+dbZ=`TcN!>OqzEhznQw>PiWz*j<}6 z;aP=|QFhs(&6{hk6hu+WbzC$Td>v_JKu>k#Jic)-pDff>rB0(r9h}J`X zsG9Le2kA 8drVM z`rbz;XXa1jceVyU{yZyH&0>*M5G<2@%_4C9b6mwkt3$`?d;L87*2?xf-g _m3yR8VyZ&b_6a;<8e=WbH8 zj>=4tIB(-wSYoWTG87}WAm3kNWqVoBU)(vH z<^39ygKg?OLw@rWfGKD?`uA`*9ozhZ(GgD+aUati5|$a(R>+LxfN&oRQ0JxdTiXq5 ztQ`v Z&uH64?eNAJ* zV(047`z9gV;%>$7RVrRMTF+9p7C+O%ng2b~+5Iq^o2WO5-Q0tlExu-Mq-tAx-(OJ{ z3%h1!z2Pd3@}NGom;5Kn`tEL6x!5 ch2d02LI-rlST*nrXMgU9EtZ@D(PN*5XUkQ?XL9Rro5 Rm z5wUFEZFOBb+R-;+(#?+76*690l-B4fE_BNt8c!V9vbr}W?GW=NVNE~R597OOs3uZK z-tYQ5FFY+Cnw=+9W2<{F-T9C|I8E`>_efuPrJ02vhFG>}U%YsI!S2+Mx5+zAi(;aN zK7FFTBD!yScX8D7zUwni{?1!G7rQy*wXWXq|8%-|w$jIIwbO6L%v&1g0QsK^d@orl zn!V#|SIBzu`_iT7|M}E7-kP>hwf}6igQ>M$ql?YQn9O0VMlL*KNK%RY$Y`cLb*%2E z;oTE$k>-cERrcxxt8RR>cF9nb<;9Nk_G^D8tGCio!Ca}oyP1`q%pGslUgb?=dodi? z{W$Ajn%j~BYJsXR)r(%h1vh%@=iFy|$FBAK{gymf#$quOQh7x`j ?X->>xgXTl{So$DsA{l1FXvK8trvw}PyI~F2fk)OhhO#wrHcIB zDFu2*Cr*-Xo4j`da{Kd^bY~Il3h&JlM9LZ8q5w$kNay@;j`DVqx=w!xZvYnS6zu?( z8BOwG`*oA4AIX=ibj+hK=oRP*p0uRXc^W9`nSSuf&HJ2s-tJeixp0cxiLs{7IoC3` zv$bVtIk88EhJvgDY_C+g%J9~(N<&j}$Lfx)uWi)4zp&mo-ALJ!p`vZ;T m;%H zZ-q{qiNxuW-*-4q?$YNJxyHD6ch&gK5YO_gK+4qIX#T2O`KMq>i~f*|{0;J5E1vur zo?m`$y%q&gbyX}DHM>qVkPctGm=s8^{2grn-0bqkd^wH9?T@2}e)US(%|4zv%U=-v zycr#ES!Ul!{@zbcGwXr9<6EAO`vsPGD?h)z^R{f;)a4U@g~lX)4HgM}>>tUdm^l9Q zzG7EM(Luv3rQPih?^`l7B;8<7eP%qztCQ3V#2scZRn#ICp5pH)+Cp~891>%p1x%VZ z{{s7m_bs7-L?lQ+#vs*Jnx1a%`a!y;P1&o>^JOcm=21NH&~_lP8OKVUqkJWsRz?kZ z4?k*_BOlH^Gak`9)zVug)wg}oAR~5Y@krT{Z*NWH>laTsdz@B!|FDb8rq;y1@oJkg zR1-4aeMUI^gU++q=Qp!x^e(Y^&TRDN9^<@?L)=aQNh}lATUBRA(Bm_Qly^$?(}> z
5_4(N1GI z&xGu_v*23%)oJEqy8}*8Iv=VKesdzYT}UdVS7dl#%Vu1Zf~~yYo{nw7-_@J8UAvy& z&U$Eb-)pJ}OAEIb4ZE^~M2L@l 5iQ_$G{Y2`+vOZ-sQT#2=53hHY zZacJeJ304tZli(gF;5o;i=bwoXm8!m5f# bfMAZPtm=J;A$__Shc*TBy^_qb% zNaWXl`gR&SjGV>S3>Na3es#Y#>fz+yttL|? M{Ro7RiJ^p0U^TN6LTA0%? zhFwkY2i=b=L~E9^DOVP%DW#7c=k(wvizg6EWq;T zg?ehu9a(QPvE^$YC}(YG>Ypd_0$ta;CVf*2D7sO$UcuWjm0o ;&p_+QOFDZA{jlN>8v*Cck?|LG=2_HF!8g>86frV3t}$bMwj*P z=ONYq*I)hrbe|L1HlZZyr%=27cBF*u8s;l_KelZTX;^vx@4cugsj7r|25*f$CJ&Hu zdj>i?onf;8z>b8$-;Ngl`@BQ1 ~W-^ ztAxRYvs%XezhCdfuPcDpIpd@|u&=}~t3vOIRFS*|j~^$H!%8CxVa9a1ga-1^i})`R z!RrT|=C{l{ePNr4uQ7A<<*aF7()_u_$!>btW60q|Y7Gm7D{F=BapQ!3<=*?&=9U+% z=}^=2I?bn6TE`>W=-$H3iS%E71Yg{PW9}NpCg=4?w1O}*1{@2`WnJzmVsBWG9_d z86Gne>6P<)KfEscFeGzc^93_4^+AGR1}PQXVjT8Lo~!km{ml0M@hTR!pm@dK10xK$ z2N?Pet(Ro$+sxiIDA>OC-FdH^>`lw5KLP_b1fi;&35mSmD_<*Da!=fx(At>2y~_U8 z+Ls__NndWIWMl|5-JAO_Pqn$;lf9FAZCto_V5H%v)3VIJrZ6ZkT%bvY#d-%XV=Ma8 zrbf!2c$2jx;` *XC3So%%3W^bxw2bxYKCmQhOWN1 zN&N4HsHvB*R6hYc2-JH(`aghP-a{@uO8cKgrA?kcCL!?$646UgXA*KfDEHK1@SXO( zykC%>Mt;~oxdtwj(=-SoUqm3G(3>xJgM)XTI(sydje`SxVCqv>H@BYdZnVQl{6to} z=x2$V`QJt%Bnt^6_C23E6sxHt{|{f^9gp??{%_AaWY18Nanqd@5|JIUv-b>TlZ@<) z5VBWhgpBM>LS|%zkd *>_ULgQr*lf~_xm-j>v=s#_3hJY7_Ce@ zS`>e4Ea!@Z@g 1CYdK|} Y(+Z1ANKP#2!?+@WpSUEd zIz~W105T!(FIo1T%-C+sgoZ{WxDZ* YNfl}2tKe62%#n!owIujN@|`CSzia7T>o!8PPi0>4fe zrgnd~p%0tBwfiCIqrAndXmRPM(to&CB28-SYc(c4=qE{n*ocBGnWCmgI8Qn*Or}*G z1%8iCVsBLZL$MOv?MObhPWVSPmF9`<`7RVBQ-#FoAy-{hRL20Hg(nbsf)!zoikC$w zpGfYU4tv_5r3TTxD8#*H3%So$`I^PI*v!aqXOE;O_aJof@X$wt8bC)&OUp`Hfk+wZ zP;Wo&?RxE;#LYBq4!YCEK^D>%g0nwr6#(f;E1Z5{=8$Tk6#L NoJ1%DMrV~_kd+- Zv5HIEwV6R79C;=SH>4 z&hpJDkyc9E;LtOfT*&Q 12U1$!TS*Uo^!I zSPe9T&VKtP7@l5;1r|A|-Nsg~i1$DS#sYc3A6#Ep6QD}TDk*`S)+w}LLf@=Q4-14{ zSqd@MoS;Gf7;sbNG&mMLl6l@b2lhFsjkfWc%u@0c6tg@ z<2f)H+Hc=Z;W-Bc1i(@gWfno?wY6ny-ft}fDhCWYKmJ^ifPpo*)joLO$sD4pqM{g& zm(36cBtZUOH{v @7+&Z^ {O-UH+0!NI}MtqMT<_A43+ z9Ld`)RykkY=TEMF08J*0nkV>*wLffN$1DNIaE?$uGB-Lp8Z5$^hD(bH#FADuNi0RU z+sCZWclF$;`-nm$FIOg{ec~zr88 yF~z?=7BOmk0958(O+-jU!$ zE>ojrAw^LlXu+BnLv?6!!Vs4Ch2Xn`C(^`I>WSKuTKceS@C9GdI-Y|g!O7k}bve?3 z@w)g2E4M9E1(_DfyvpZ3nc-R0J}fVZ;d8;>+_Mw9Z<06q25e(P--?HZx-~FMJjJ4a zPWQMpb 1at|lGr0%j zFTehFMBZcaxaS$3`-aPA@#-Ha!%zq>15&R&aY~O*o WzdDrk#FZ*u?vrg4+e6Hy~W+n?iJA8B2=nUZso~d^eV4(Z5 zriSKA6nJV)fl+c73ItKk>cXbK_V*PoK8FJzm(%A;O*|}DNX`NMsc37Y$ipwbu ^w!I`h*891@bf;CMynPlIB4G#)2HZ=us_5zP$ybwf( z*zYdBxI`qg7)QNYW{Wy|`AA+zC#uGPy89Lz)-I`TaM`n;KV7`XrB4$2IEHs) I*`>T9s^FC@0MxTHT27bptW%HA zTQFx|?~Y3~y8g3Ht7H1)WwZ!m7+7I_9392cLcLVCgP6+f;m0i|=o~73uf@&PPbo?Y z3gJ tQ?(mKMXi5HM!|Tncl;b{|Kr zYUUo`lJtZLB5reIVr3$(WZ4}}0zbvJq$$47ij4x}g0 H_}fVc7bHE;SLSVL*^2unz8e6_~AU5?=m zi73b8wGJY>wzqIw23= zok}lo#7_w8BOYkHs7X9LJ%Kg=4-e+oF#l=#mKgVv*;1?RiF|4xvgu-pn_6AOo!gvF zmi)=V>{a8FbCopw;XEzRLKvfk%qo!qfqX8)i-ght)x&U= yH zu3eL$luDT!(a3(SXF;hlQWTxNd9wZDtct1v_Y-}XWeuh$eH AS% zkdYTT8eCAC8Xq_)MfQ$Nnh=>ROk;)yPsaqEewXL1zj2tEb015ggpn~l0*19fvJ^E% z65-4;!4aFOW_YeB1~tuWXm*A-Wfe~l{I)mIeL8st6)k&_J< *sDq7pc5SzT*i1?Af?smX3WMgh9$J+tmSwTr %+vTdGd?JKA(cjfAqT7$ zU~!4YkcM}m(if`ka3+9JgBM0SVd~t0pABLxR%i_Z4w3^JKw1XWbID@EhalD{a0jpY zRYN8TBW8yO^zsrHuCAVHc}n$G9Yevzf#G@(mwSf9c}tu^!3Hi3HDFtEIVE#3RMOr% zr=;Yk_v}y4Q6VRY4$I5ya{05Yd8T;_I~qxhp{vh~O|0$Gq-AlgKo(=(@EUq^(;w~u z=C$9ebV~8 {@$!|ABo`aV~|CiD?)% zgvH$qVAy~U0)mXp;D)EO^9U-Mdr-K3WOnu#^!(GuppiZHmjY6crDd3TihPzj*C)t; z1ZNWH)D0aV|NPt%y;A(>`rBzSaCirQ7Ram|hsOsA#1QEKC1$n#7-w=?hzxuBevUak zSg-OTmAHY<1zU^yfMlqv) Sf?aevkWqt$z0pV- E*2=2=eS=( ?!m$*cc8m$UJ}e9x7P8O9^=)jn#M4|DLdUGM(nJLjmr_-11XAINI5Rsr zIwEbXtHw 0vW$R8^^SC!YnC<}0Pu-eG--RT1xjrJ4Wd-rUgVH1!?0V58+Bk=Jx2 x6*8&7Z#QnPpOMGb&8JNWFdK9e^`J?~#3AS86^tFwuez32j_s}E*s`REZq zSJzY?{K(gZg5I047O0c(K{0N-b_)TgYXOpAYs7|hYnTN;#VBARxqusz)Is0fs_6~7 z*6mR6y2c1aKysGl4