Merge branch 'pyside6' of https://git.cckb9998.synology.me/ckh08045/autoTrans into pyside6
132821
appTranslator.log
|
|
@ -116,10 +116,10 @@ class BrowserController:
|
||||||
|
|
||||||
# 기본 페이지가 없을 수 있으므로 새로운 페이지 생성
|
# 기본 페이지가 없을 수 있으므로 새로운 페이지 생성
|
||||||
self.page = await self.browser.new_page()
|
self.page = await self.browser.new_page()
|
||||||
self.logger.debug('새 페이지 로딩 중...')
|
self.logger.info('새 페이지 로딩 중...')
|
||||||
|
|
||||||
await self.page.goto('https://percenty.co.kr/signin')
|
await self.page.goto('https://percenty.co.kr/signin')
|
||||||
self.logger.debug('percenty.co.kr/signin 로딩 완료')
|
self.logger.info('percenty.co.kr/signin 로딩 완료')
|
||||||
|
|
||||||
# 첫 번째 기본 탭 닫기
|
# 첫 번째 기본 탭 닫기
|
||||||
if self.browser.pages:
|
if self.browser.pages:
|
||||||
|
|
@ -132,7 +132,7 @@ class BrowserController:
|
||||||
# 창 핸들 찾기 (동적으로 얻은 페이지 제목 사용)
|
# 창 핸들 찾기 (동적으로 얻은 페이지 제목 사용)
|
||||||
self.chrome_hwnd = self.find_window_by_title(page_title)
|
self.chrome_hwnd = self.find_window_by_title(page_title)
|
||||||
if not self.chrome_hwnd:
|
if not self.chrome_hwnd:
|
||||||
self.logger.debug('크롬 창을 찾을 수 없습니다.')
|
self.logger.warning('크롬 창을 찾을 수 없습니다.')
|
||||||
else:
|
else:
|
||||||
self.logger.debug(f'크롬 창 핸들: {self.chrome_hwnd}')
|
self.logger.debug(f'크롬 창 핸들: {self.chrome_hwnd}')
|
||||||
|
|
||||||
|
|
@ -140,7 +140,7 @@ class BrowserController:
|
||||||
|
|
||||||
async def login(self, admin_id, user_id, admin_password, user_password, is_admin=False):
|
async def login(self, admin_id, user_id, admin_password, user_password, is_admin=False):
|
||||||
"""로그인 처리"""
|
"""로그인 처리"""
|
||||||
self.logger.debug(f'로그인 시도 중: {"관리자" if is_admin else "직원"} 계정')
|
self.logger.info(f'로그인 시도 중: {"관리자" if is_admin else "직원"} 계정')
|
||||||
|
|
||||||
if is_admin:
|
if is_admin:
|
||||||
# 관리자 로그인 처리
|
# 관리자 로그인 처리
|
||||||
|
|
@ -158,7 +158,7 @@ class BrowserController:
|
||||||
await self.page.fill(self.login_password_locator, user_password)
|
await self.page.fill(self.login_password_locator, user_password)
|
||||||
await self.page.click(self.staff_login_button_locator)
|
await self.page.click(self.staff_login_button_locator)
|
||||||
|
|
||||||
self.logger.debug(f'로그인 완료: {"관리자" if is_admin else "직원"} 계정')
|
self.logger.info(f'로그인 완료: {"관리자" if is_admin else "직원"} 계정')
|
||||||
|
|
||||||
# await self.page.wait_for_load_state('networkidle', timeout=10000)
|
# await self.page.wait_for_load_state('networkidle', timeout=10000)
|
||||||
|
|
||||||
|
|
@ -169,7 +169,7 @@ class BrowserController:
|
||||||
if self.browser:
|
if self.browser:
|
||||||
await self.browser.close()
|
await self.browser.close()
|
||||||
await self.playwright.stop()
|
await self.playwright.stop()
|
||||||
self.logger.debug('브라우저 종료됨.')
|
self.logger.info('브라우저 종료됨.')
|
||||||
|
|
||||||
def find_window_by_title(self, window_name):
|
def find_window_by_title(self, window_name):
|
||||||
"""창 제목을 통해 핸들을 찾는 메서드"""
|
"""창 제목을 통해 핸들을 찾는 메서드"""
|
||||||
|
|
@ -187,7 +187,7 @@ class BrowserController:
|
||||||
win32gui.SetForegroundWindow(self.chrome_hwnd)
|
win32gui.SetForegroundWindow(self.chrome_hwnd)
|
||||||
self.logger.debug('크롬 창으로 포커스 이동.')
|
self.logger.debug('크롬 창으로 포커스 이동.')
|
||||||
else:
|
else:
|
||||||
self.logger.debug('크롬 창을 찾을 수 없습니다.')
|
self.logger.error('크롬 창을 찾을 수 없습니다.')
|
||||||
|
|
||||||
|
|
||||||
async def get_total_product_count_ori(self):
|
async def get_total_product_count_ori(self):
|
||||||
|
|
@ -219,107 +219,99 @@ class BrowserController:
|
||||||
}}''', self.total_product_count_locator)
|
}}''', self.total_product_count_locator)
|
||||||
|
|
||||||
if element_text:
|
if element_text:
|
||||||
self.logger.debug(f"총 상품수 확인: {element_text}") # 텍스트 확인용 로그
|
self.logger.info(f"총 상품수 확인: {element_text}") # 텍스트 확인용 로그
|
||||||
# "총 xx개 상품"에서 숫자만 추출
|
# "총 xx개 상품"에서 숫자만 추출
|
||||||
count = int(''.join(filter(str.isdigit, element_text)))
|
count = int(''.join(filter(str.isdigit, element_text)))
|
||||||
return count
|
return count
|
||||||
else:
|
else:
|
||||||
self.logger.debug("요소를 찾을 수 없습니다.")
|
self.logger.warning("요소를 찾을 수 없습니다.")
|
||||||
return 0
|
return 0
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.logger.debug(f"상품 수를 가져오는 중 오류 발생: {e}", exc_info=True)
|
self.logger.error(f"상품 수를 가져오는 중 오류 발생: {e}", exc_info=True)
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
|
|
||||||
# async def get_product_name(self, index, selector='xpath'):
|
# def fetch_image_urls_ori(self, html_content):
|
||||||
# """
|
# """
|
||||||
# 상품명을 수집하는 메서드
|
# HTML 콘텐츠에서 모든 <img> 태그의 URL을 순서대로 추출하고 중복 제거.
|
||||||
# index : 상품명을 수집하는 인덱스
|
|
||||||
# selector : 수집방법 (css 또는 xpath)
|
|
||||||
# """
|
# """
|
||||||
# try:
|
# soup = BeautifulSoup(html_content, 'html.parser')
|
||||||
# # config.ini에서 설정된 선택자에 인덱스를 적용하여 가져옴
|
|
||||||
# # product_name_selector = self.product_name_template.format(index=index)
|
|
||||||
# # self.logger.debug(f"사용된 선택자: {product_name_selector}") # 선택자 출력
|
|
||||||
|
|
||||||
# product_name_xpath_selector = self.product_name_template_xpath.format(index=index)
|
# # 순서를 유지하면서 중복을 제거하기 위해 리스트 사용
|
||||||
# self.logger.debug(f"사용된 선택자: {product_name_xpath_selector}") # 선택자 출력
|
# image_urls = []
|
||||||
|
# seen_urls = set()
|
||||||
|
|
||||||
|
# # <figure class="image"> 내부의 모든 <img> 태그 찾기
|
||||||
|
# figures = soup.find_all('figure', class_='image')
|
||||||
|
# for figure in figures:
|
||||||
|
# img_tag = figure.find('img')
|
||||||
|
# if img_tag and 'src' in img_tag.attrs:
|
||||||
|
# url = img_tag['src']
|
||||||
|
# if url not in seen_urls:
|
||||||
|
# image_urls.append(url)
|
||||||
|
# seen_urls.add(url) # 중복 방지
|
||||||
|
|
||||||
# # XPath 기반으로 요소 검색
|
# # class="image_resized"를 가진 모든 <img> 태그 찾기
|
||||||
# product_name_element = await self.page.locator(f"xpath={product_name_xpath_selector}").element_handle()
|
# images_resized = soup.find_all('img', class_='image_resized')
|
||||||
|
# for img in images_resized:
|
||||||
# # product_name_element = await self.page.query_selector(product_name_selector)
|
# if img and 'src' in img.attrs:
|
||||||
|
# url = img['src']
|
||||||
# # product_name_element가 None인지 확인
|
# if url not in seen_urls:
|
||||||
# if product_name_element is None:
|
# image_urls.append(url)
|
||||||
# self.logger.error(f"상품명 요소를 찾을 수 없습니다: index {index}")
|
# seen_urls.add(url) # 중복 방지
|
||||||
# return "수집 오류 발생"
|
|
||||||
|
|
||||||
# # 요소가 존재할 경우, inner_text 수집
|
|
||||||
# product_name = await product_name_element.inner_text()
|
|
||||||
# return product_name.strip()
|
|
||||||
# except Exception as e:
|
|
||||||
# self.logger.error(f"상품명 수집 중 오류: {e}")
|
|
||||||
# return "수집 오류 발생"
|
|
||||||
|
|
||||||
|
|
||||||
|
# return image_urls
|
||||||
|
|
||||||
def fetch_image_urls(self, html_content):
|
def fetch_image_urls(self, html_content):
|
||||||
"""
|
"""
|
||||||
HTML 콘텐츠에서 모든 <img> 태그의 URL을 순서대로 추출하고 중복 제거.
|
HTML 콘텐츠에서 모든 <img> 태그의 URL을 추출하는 함수.
|
||||||
|
<figure> 안의 <img> 태그와 독립된 <img> 태그 모두 처리.
|
||||||
"""
|
"""
|
||||||
soup = BeautifulSoup(html_content, 'html.parser')
|
soup = BeautifulSoup(html_content, 'html.parser')
|
||||||
|
|
||||||
# 중복된 이미지를 제거하기 위해 set 사용
|
# 모든 <img> 태그를 찾기
|
||||||
image_urls_set = set()
|
image_urls = []
|
||||||
|
img_tags = soup.find_all('img')
|
||||||
|
|
||||||
# class="image_resized"를 가진 모든 <img> 태그 찾기
|
for img in img_tags:
|
||||||
images_resized = soup.find_all('img', class_='image_resized')
|
# img 태그에서 src 속성 추출
|
||||||
for img in images_resized:
|
if 'src' in img.attrs:
|
||||||
if img and 'src' in img.attrs:
|
image_url = img['src']
|
||||||
image_urls_set.add(img['src']) # 중복을 방지하기 위해 set에 추가
|
image_urls.append(image_url)
|
||||||
|
self.logger.debug(f"fetch_image_urls 에서 추출한 이미지URL 갯수 : {len(image_urls)} 개")
|
||||||
|
|
||||||
# <figure class="image"> 내부의 모든 <img> 태그 찾기
|
self.logger.debug(f"fetch_image_urls 에서 추출한 이미지URL 목록 : {image_urls}")
|
||||||
figures = soup.find_all('figure', class_='image')
|
|
||||||
for figure in figures:
|
|
||||||
img_tag = figure.find('img')
|
|
||||||
if img_tag and 'src' in img_tag.attrs:
|
|
||||||
image_urls_set.add(img_tag['src']) # 중복을 방지하기 위해 set에 추가
|
|
||||||
|
|
||||||
# set을 list로 변환하여 반환 (순서 유지가 필요하면 set 대신 리스트로 처리해야 함)
|
|
||||||
image_urls = list(image_urls_set)
|
|
||||||
return image_urls
|
return image_urls
|
||||||
|
|
||||||
|
|
||||||
async def close_ad_if_exists(self):
|
async def close_ad_if_exists(self):
|
||||||
|
|
||||||
"""광고 다이얼로그가 있으면 닫기 버튼을 클릭하는 메서드"""
|
"""광고 다이얼로그가 있으면 닫기 버튼을 클릭하는 메서드"""
|
||||||
try:
|
try:
|
||||||
# 광고 다이얼로그가 나타날 때까지 기다림
|
# 광고 다이얼로그가 나타날 때까지 기다림
|
||||||
await self.page.wait_for_selector(self.close_ad_dialog_locator, timeout=3000)
|
await self.page.wait_for_selector(self.close_ad_dialog_locator, timeout=3000, state='visible')
|
||||||
self.logger.debug("다이얼로그가 발견되었습니다. 닫기 버튼을 클릭합니다.")
|
self.logger.info("다이얼로그가 발견되었습니다. 닫기 버튼을 클릭합니다.")
|
||||||
|
|
||||||
# 닫기 버튼 클릭
|
# 닫기 버튼 클릭
|
||||||
close_button = await self.page.query_selector(self.close_ad_button_locator)
|
close_button = await self.page.query_selector(self.close_ad_button_locator)
|
||||||
if close_button:
|
if close_button:
|
||||||
await close_button.click()
|
await close_button.click()
|
||||||
self.logger.debug("다이얼로그를 성공적으로 닫았습니다.")
|
self.logger.info("다이얼로그를 성공적으로 닫았습니다.")
|
||||||
else:
|
else:
|
||||||
self.logger.debug("닫기 버튼을 찾지 못했습니다.")
|
self.logger.warning("닫기 버튼을 찾지 못했습니다.")
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
# 다이얼로그가 없거나 다른 문제가 발생한 경우
|
# 다이얼로그가 없거나 다른 문제가 발생한 경우
|
||||||
self.logger.debug(f"다이얼로그가 발견되지 않았거나 오류 발생: {e}", exc_info=True)
|
self.logger.error(f"다이얼로그가 발견되지 않았거나 오류 발생: {e}", exc_info=True)
|
||||||
|
|
||||||
async def go_to_new_product_page(self):
|
async def go_to_new_product_page(self):
|
||||||
"""신규 상품 등록 페이지로 이동"""
|
"""신규 상품 등록 페이지로 이동"""
|
||||||
try:
|
try:
|
||||||
new_product_page_locator = self.locator_manager.get_locator('BrowserControl', 'new_product_page_locator')
|
new_product_page_locator = self.locator_manager.get_locator('BrowserControl', 'new_product_page_locator')
|
||||||
await self.page.click(new_product_page_locator)
|
await self.page.click(new_product_page_locator)
|
||||||
self.logger.debug("신규 상품 등록 페이지로 이동 완료.")
|
self.logger.info("신규 상품 등록 페이지로 이동 완료.")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.logger.debug(f"신규 상품 등록 페이지 이동 중 오류: {e}", exc_info=True)
|
self.logger.error(f"신규 상품 등록 페이지 이동 중 오류: {e}", exc_info=True)
|
||||||
|
|
||||||
async def get_product_edit_buttons(self):
|
async def get_product_edit_buttons(self):
|
||||||
"""현재 페이지의 세부사항 수정 및 업로드 버튼을 찾기"""
|
"""현재 페이지의 세부사항 수정 및 업로드 버튼을 찾기"""
|
||||||
|
|
@ -328,7 +320,7 @@ class BrowserController:
|
||||||
edit_button_selector = self.product_edit_button
|
edit_button_selector = self.product_edit_button
|
||||||
|
|
||||||
if not edit_button_selector:
|
if not edit_button_selector:
|
||||||
self.logger.debug("상품 수정 버튼의 선택자를 찾을 수 없습니다.")
|
self.logger.warning("상품 수정 버튼의 선택자를 찾을 수 없습니다.")
|
||||||
return []
|
return []
|
||||||
|
|
||||||
# 선택자를 사용해 버튼 객체를 찾음
|
# 선택자를 사용해 버튼 객체를 찾음
|
||||||
|
|
@ -336,17 +328,17 @@ class BrowserController:
|
||||||
|
|
||||||
# 버튼이 존재하는지 확인
|
# 버튼이 존재하는지 확인
|
||||||
if await buttons.count() == 0:
|
if await buttons.count() == 0:
|
||||||
self.logger.debug("세부사항 수정 및 업로드 버튼을 찾을 수 없습니다.")
|
self.logger.warning("세부사항 수정 및 업로드 버튼을 찾을 수 없습니다.")
|
||||||
return []
|
return []
|
||||||
|
|
||||||
count = await buttons.count()
|
count = await buttons.count()
|
||||||
self.logger.debug(f"수정할 상품 개수: {count}")
|
self.logger.info(f"수정할 상품 개수: {count}")
|
||||||
|
|
||||||
# 모든 버튼을 리스트로 반환
|
# 모든 버튼을 리스트로 반환
|
||||||
return [buttons.nth(i) for i in range(count)]
|
return [buttons.nth(i) for i in range(count)]
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.logger.debug(f"상품 수정 버튼을 찾는 중 오류: {e}", exc_info=True)
|
self.logger.error(f"상품 수정 버튼을 찾는 중 오류: {e}", exc_info=True)
|
||||||
return []
|
return []
|
||||||
|
|
||||||
async def get_product_edit_buttons_by_templete(self):
|
async def get_product_edit_buttons_by_templete(self):
|
||||||
|
|
@ -361,16 +353,16 @@ class BrowserController:
|
||||||
# 버튼이 존재하는지 확인
|
# 버튼이 존재하는지 확인
|
||||||
button_count = await buttons.count()
|
button_count = await buttons.count()
|
||||||
if button_count == 0:
|
if button_count == 0:
|
||||||
self.logger.debug("세부사항 수정 및 업로드 버튼을 찾을 수 없습니다.")
|
self.logger.warning("세부사항 수정 및 업로드 버튼을 찾을 수 없습니다.")
|
||||||
return []
|
return []
|
||||||
|
|
||||||
self.logger.debug(f"현재 페이지의 수정할 상품 개수: {button_count}")
|
self.logger.info(f"현재 페이지의 수정할 상품 개수: {button_count}")
|
||||||
|
|
||||||
# 모든 버튼을 리스트로 반환
|
# 모든 버튼을 리스트로 반환
|
||||||
return [buttons.nth(i) for i in range(button_count)]
|
return [buttons.nth(i) for i in range(button_count)]
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.logger.debug(f"상품 수정 버튼을 찾는 중 오류: {e}", exc_info=True)
|
self.logger.error(f"상품 수정 버튼을 찾는 중 오류: {e}", exc_info=True)
|
||||||
return []
|
return []
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -387,11 +379,11 @@ class BrowserController:
|
||||||
if button:
|
if button:
|
||||||
await button.scroll_into_view_if_needed()
|
await button.scroll_into_view_if_needed()
|
||||||
await self.page.evaluate('arguments[0].click();', button)
|
await self.page.evaluate('arguments[0].click();', button)
|
||||||
self.logger.debug(f'{index}번째 상품의 수정 버튼 클릭 완료')
|
self.logger.info(f'{index}번째 상품의 수정 버튼 클릭 완료')
|
||||||
else:
|
else:
|
||||||
self.logger.debug(f'{index}번째 상품의 수정 버튼을 찾지 못했습니다.')
|
self.logger.warning(f'{index}번째 상품의 수정 버튼을 찾지 못했습니다.')
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.logger.debug(f'{index}번째 상품의 수정 버튼 클릭 중 오류: {str(e)}')
|
self.logger.error(f'{index}번째 상품의 수정 버튼 클릭 중 오류: {str(e)}')
|
||||||
|
|
||||||
|
|
||||||
async def open_product_edit_dialog(self, button):
|
async def open_product_edit_dialog(self, button):
|
||||||
|
|
@ -402,42 +394,42 @@ class BrowserController:
|
||||||
self.logger.debug("상품의 '세부사항 수정 및 업로드' 버튼을 화면에 보이도록 스크롤.")
|
self.logger.debug("상품의 '세부사항 수정 및 업로드' 버튼을 화면에 보이도록 스크롤.")
|
||||||
|
|
||||||
await button.click()
|
await button.click()
|
||||||
self.logger.debug("세부사항 수정 다이얼로그 열기 완료.")
|
self.logger.info("세부사항 수정 다이얼로그 열기 완료.")
|
||||||
await self.page.wait_for_selector('div.ant-tabs-nav') # 다이얼로그가 완전히 로딩될 때까지 기다림
|
await self.page.wait_for_selector('div.ant-tabs-nav') # 다이얼로그가 완전히 로딩될 때까지 기다림
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.logger.debug(f"세부사항 수정 다이얼로그 열기 중 오류: {e}", exc_info=True)
|
self.logger.error(f"세부사항 수정 다이얼로그 열기 중 오류: {e}", exc_info=True)
|
||||||
|
|
||||||
async def click_detail_tab(self):
|
async def click_detail_tab(self):
|
||||||
"""상세페이지 탭 클릭"""
|
"""상세페이지 탭 클릭"""
|
||||||
try:
|
try:
|
||||||
await self.page.click(self.detail_tab_locator)
|
await self.page.click(self.detail_tab_locator)
|
||||||
self.logger.debug("상세페이지 탭 클릭 완료.")
|
self.logger.info("상세페이지 탭 클릭 완료.")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.logger.debug(f"상세페이지 탭 클릭 중 오류: {e}", exc_info=True)
|
self.logger.error(f"상세페이지 탭 클릭 중 오류: {e}", exc_info=True)
|
||||||
|
|
||||||
async def click_option_tab(self):
|
async def click_option_tab(self):
|
||||||
"""옵션 탭 클릭"""
|
"""옵션 탭 클릭"""
|
||||||
try:
|
try:
|
||||||
await self.page.click(self.option_tab_locator)
|
await self.page.click(self.option_tab_locator)
|
||||||
self.logger.debug("옵션 탭 클릭 완료.")
|
self.logger.info("옵션 탭 클릭 완료.")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.logger.debug(f"옵션 탭 클릭 중 오류: {e}", exc_info=True)
|
self.logger.error(f"옵션 탭 클릭 중 오류: {e}", exc_info=True)
|
||||||
|
|
||||||
async def click_price_tab(self):
|
async def click_price_tab(self):
|
||||||
"""가격 탭 클릭"""
|
"""가격 탭 클릭"""
|
||||||
try:
|
try:
|
||||||
await self.page.click(self.price_tab_locator)
|
await self.page.click(self.price_tab_locator)
|
||||||
self.logger.debug("가격 탭 클릭 완료.")
|
self.logger.info("가격 탭 클릭 완료.")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.logger.debug(f"가격 탭 클릭 중 오류: {e}", exc_info=True)
|
self.logger.error(f"가격 탭 클릭 중 오류: {e}", exc_info=True)
|
||||||
|
|
||||||
async def click_title_tab(self):
|
async def click_title_tab(self):
|
||||||
"""상품명 탭 클릭"""
|
"""상품명 탭 클릭"""
|
||||||
try:
|
try:
|
||||||
await self.page.click(self.title_tab_locator)
|
await self.page.click(self.title_tab_locator)
|
||||||
self.logger.debug("상품명 탭 클릭 완료.")
|
self.logger.info("상품명 탭 클릭 완료.")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.logger.debug(f"상품명 탭 클릭 중 오류: {e}", exc_info=True)
|
self.logger.error(f"상품명 탭 클릭 중 오류: {e}", exc_info=True)
|
||||||
|
|
||||||
async def extract_image_urls(self, optionHandler, is_option_data=False):
|
async def extract_image_urls(self, optionHandler, is_option_data=False):
|
||||||
"""상세페이지에서 이미지 URL 추출"""
|
"""상세페이지에서 이미지 URL 추출"""
|
||||||
|
|
@ -452,13 +444,13 @@ class BrowserController:
|
||||||
|
|
||||||
|
|
||||||
# 'data-value' 속성 값을 추출 (textarea 요소)
|
# 'data-value' 속성 값을 추출 (textarea 요소)
|
||||||
textarea = await self.page.wait_for_selector(ck_source_editing_area_locator)
|
textarea = await self.page.wait_for_selector(ck_source_editing_area_locator, timeout=5000)
|
||||||
data_value = await textarea.get_attribute("data-value")
|
data_value = await textarea.get_attribute("data-value")
|
||||||
|
|
||||||
|
|
||||||
# HTML 소스에서 이미지 URL 추출
|
# HTML 소스에서 이미지 URL 추출
|
||||||
image_urls = self.fetch_image_urls(data_value)
|
image_urls = self.fetch_image_urls(data_value)
|
||||||
self.logger.debug(f'추출된 이미지 URL 수: {len(image_urls)}')
|
self.logger.info(f'추출된 이미지 URL 수: {len(image_urls)}')
|
||||||
|
|
||||||
# HTML 소스에서 이미지 URL 삭제
|
# HTML 소스에서 이미지 URL 삭제
|
||||||
self.logger.debug('img 태그를 삭제 중...')
|
self.logger.debug('img 태그를 삭제 중...')
|
||||||
|
|
@ -479,12 +471,18 @@ class BrowserController:
|
||||||
|
|
||||||
if is_option_data:
|
if is_option_data:
|
||||||
self.logger.debug('옵션 데이터 입력 시작')
|
self.logger.debug('옵션 데이터 입력 시작')
|
||||||
|
option_data = {} # option_data 초기화
|
||||||
option_data = optionHandler.get_selected_translated_options()
|
option_data = optionHandler.get_selected_translated_options()
|
||||||
|
is_single = optionHandler.option_info['is_single_option']
|
||||||
|
|
||||||
|
is_single = True # 옵션입력 일단 제외
|
||||||
|
self.logger.debug('옵션입력 일단 제외')
|
||||||
|
|
||||||
self.logger.debug('가져온 옵션 데이터')
|
self.logger.debug('가져온 옵션 데이터')
|
||||||
self.logger.debug(f'{option_data}')
|
self.logger.debug(f'{option_data}')
|
||||||
|
|
||||||
# 옵션 입력 필드 선택
|
# 옵션 입력 필드 선택
|
||||||
input_field = await self.page.wait_for_selector(self.option_input_field_locator)
|
input_field = await self.page.wait_for_selector(self.option_input_field_locator, timeout=5000)
|
||||||
await input_field.press('Enter')
|
await input_field.press('Enter')
|
||||||
|
|
||||||
# 선두부 텍스트 입력
|
# 선두부 텍스트 입력
|
||||||
|
|
@ -493,79 +491,86 @@ class BrowserController:
|
||||||
if 'leading_text' in key and leading_text: # leading_text 항목만 가져오기
|
if 'leading_text' in key and leading_text: # leading_text 항목만 가져오기
|
||||||
await input_field.type(leading_text)
|
await input_field.type(leading_text)
|
||||||
await input_field.press('Enter')
|
await input_field.press('Enter')
|
||||||
self.logger.debug(f"{key} 텍스트 입력 완료: {leading_text}")
|
self.logger.info(f"{key} 텍스트 입력 완료: {leading_text}")
|
||||||
|
|
||||||
|
if not is_single:
|
||||||
|
self.logger.info('단일옵션이 아니므로 옵션목록을 입력')
|
||||||
|
|
||||||
# 각 옵션을 한 줄씩 입력
|
# 각 옵션을 한 줄씩 입력
|
||||||
await input_field.press('Enter')
|
await input_field.type("# 옵션 목록")
|
||||||
await input_field.type("# > 옵션 목록")
|
|
||||||
await input_field.press('Enter')
|
|
||||||
await input_field.press('Enter')
|
await input_field.press('Enter')
|
||||||
|
|
||||||
|
# 첫 번째 옵션의 번역된 옵션명만 입력
|
||||||
# 첫 번째 옵션에만 - 기호를 붙여 목록 시작
|
first_key = list(option_data.keys())[0]
|
||||||
await input_field.type(f"- A. {option_data[0]}")
|
first_value = option_data[first_key]
|
||||||
|
await input_field.type(f"- 1. {first_value}")
|
||||||
await input_field.press('Enter') # 첫 번째 옵션 이후 엔터로 줄바꿈
|
await input_field.press('Enter') # 첫 번째 옵션 이후 엔터로 줄바꿈
|
||||||
|
|
||||||
# 나머지 옵션들은 - 없이 입력하여 마크다운 목록으로 표시
|
# 나머지 옵션도 번역된 옵션명만 입력
|
||||||
for i, option in enumerate(option_data[1:], start=2):
|
for i, (key, value) in enumerate(list(option_data.items())[1:], start=2):
|
||||||
option_text = option[0] if isinstance(option, tuple) else option
|
await input_field.type(f"{i}. {value}") # 옵션 번호와 번역된 옵션명만 입력
|
||||||
option_prefix = f"{chr(64 + i)}. "
|
|
||||||
await input_field.type(option_prefix + option_text)
|
|
||||||
await input_field.press('Enter') # 엔터 키를 입력하여 줄바꿈
|
await input_field.press('Enter') # 엔터 키를 입력하여 줄바꿈
|
||||||
|
|
||||||
# 목록 끝을 알리기 위해 엔터 두 번 입력
|
# 목록 끝을 알리기 위해 엔터 두 번 입력
|
||||||
await input_field.press('Enter')
|
await input_field.press('Enter')
|
||||||
await input_field.press('Enter')
|
await input_field.press('Enter')
|
||||||
|
|
||||||
|
|
||||||
# 후두부 텍스트 입력
|
# 후두부 텍스트 입력
|
||||||
await input_field.type('---')
|
|
||||||
await input_field.type('### 나열된 옵션목록 이외의 옵션이 필요하실 경우 고객센터로 연락주세요.')
|
await input_field.type('### 나열된 옵션목록 이외의 옵션이 필요하실 경우 고객센터로 연락주세요.')
|
||||||
await input_field.press('Enter')
|
await input_field.press('Enter')
|
||||||
await input_field.type('---')
|
await input_field.type('---')
|
||||||
await input_field.press('Enter')
|
await input_field.press('Enter')
|
||||||
|
|
||||||
self.logger.debug('옵션 데이터 입력 완료 후 엔터 입력')
|
self.logger.info('옵션 데이터 입력 완료')
|
||||||
|
|
||||||
return image_urls
|
return image_urls
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.logger.debug(f"이미지 URL 추출 중 오류: {e}", exc_info=True)
|
self.logger.error(f"이미지 URL 추출 & 옵션데이터 입력 처리 중 오류: {e}", exc_info=True)
|
||||||
return []
|
return image_urls if image_urls else []
|
||||||
|
|
||||||
async def paste_image_in_chrome(self, clipboardImageManager, url):
|
def paste_image_in_chrome(self, clipboardImageManager, url):
|
||||||
"""크롬으로 포커스를 옮기고 클립보드의 이미지를 붙여넣고 엔터 입력"""
|
"""크롬으로 포커스를 옮기고 클립보드의 이미지를 붙여넣고 엔터 입력"""
|
||||||
self.logger.debug("크롬으로 포커스를 옮기고 클립보드의 이미지를 붙여넣고 엔터 입력")
|
self.logger.debug("크롬으로 포커스를 옮기고 클립보드의 이미지를 붙여넣고 엔터 입력")
|
||||||
try:
|
try:
|
||||||
self.switch_to_chrome() # 크롬으로 포커스 이동
|
self.switch_to_chrome() # 크롬으로 포커스 이동
|
||||||
await clipboardImageManager.process_clipboard(url) # 클립보드 내용을 처리
|
clipboardImageManager.process_clipboard(url) # 클립보드 내용을 처리
|
||||||
# clipboard_content = pyperclip.paste()
|
# clipboard_content = pyperclip.paste()
|
||||||
if clipboardImageManager.is_clipboard_image():
|
if clipboardImageManager.is_clipboard_image():
|
||||||
pyautogui.hotkey('ctrl', 'v') # 클립보드 이미지 붙여넣기
|
pyautogui.hotkey('ctrl', 'v') # 클립보드 이미지 붙여넣기
|
||||||
|
self.logger.info("이미지 붙여넣기 완료.")
|
||||||
pyautogui.press('right') # 오른쪽 입력
|
pyautogui.press('right') # 오른쪽 입력
|
||||||
|
<<<<<<< HEAD
|
||||||
self.logger.debug("이미지 붙여넣기 완료.")
|
self.logger.debug("이미지 붙여넣기 완료.")
|
||||||
self.logger.debug("이미지 붙여넣기 완료로 클립보드 비우기.")
|
self.logger.debug("이미지 붙여넣기 완료로 클립보드 비우기.")
|
||||||
clipboardImageManager.clear_clipboard()
|
clipboardImageManager.clear_clipboard()
|
||||||
|
=======
|
||||||
|
self.logger.info("이미지 붙여넣기 완료로 클립보드 비우기.")
|
||||||
|
clipboardImageManager.clear_clipboard()
|
||||||
|
return True
|
||||||
|
>>>>>>> f71275020a91aa828fd528172caa46a2bd494578
|
||||||
else:
|
else:
|
||||||
self.logger.debug("클립보드가 비어있습니다.")
|
self.logger.warning("클립보드가 비어있습니다.")
|
||||||
|
return False
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.logger.debug(f"이미지 붙여넣기 중 오류: {e}", exc_info=True)
|
self.logger.error(f"이미지 붙여넣기 중 오류: {e}", exc_info=True)
|
||||||
|
return False
|
||||||
|
|
||||||
async def save_and_ecs_product_edit(self):
|
async def save_and_ecs_product_edit(self):
|
||||||
"""상품 수정 후 저장 버튼 클릭"""
|
"""상품 수정 후 저장 버튼 클릭"""
|
||||||
try:
|
try:
|
||||||
await self.page.click(self.save_button_locator)
|
await self.page.click(self.save_button_locator)
|
||||||
await self.page.keyboard.press("Escape")
|
await self.page.keyboard.press("Escape")
|
||||||
self.logger.debug("상품 수정 내용 저장 및 ECS 완료.")
|
self.logger.info("상품 수정 내용 저장 및 ECS 완료.")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.logger.debug(f"저장 버튼 클릭 중 오류: {e}", exc_info=True)
|
self.logger.error(f"저장 버튼 클릭 중 오류: {e}", exc_info=True)
|
||||||
|
|
||||||
async def save_product_edit(self):
|
async def save_product_edit(self):
|
||||||
"""상품 수정 후 저장 버튼 클릭"""
|
"""상품 수정 후 저장 버튼 클릭"""
|
||||||
try:
|
try:
|
||||||
await self.page.click(self.save_button_locator)
|
await self.page.click(self.save_button_locator)
|
||||||
self.logger.debug("상품 수정 내용 저장 완료.")
|
self.logger.info("상품 수정 내용 저장 완료.")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.logger.debug(f"저장 버튼 클릭 중 오류: {e}", exc_info=True)
|
self.logger.error(f"저장 버튼 클릭 중 오류: {e}", exc_info=True)
|
||||||
|
|
||||||
async def go_to_next_page(self):
|
async def go_to_next_page(self):
|
||||||
"""다음 페이지로 이동"""
|
"""다음 페이지로 이동"""
|
||||||
|
|
@ -574,7 +579,7 @@ class BrowserController:
|
||||||
current_page = await self.page.query_selector(self.current_page_locator)
|
current_page = await self.page.query_selector(self.current_page_locator)
|
||||||
|
|
||||||
if not current_page:
|
if not current_page:
|
||||||
self.logger.debug("현재 페이지 정보를 찾을 수 없습니다.")
|
self.logger.warning("현재 페이지 정보를 찾을 수 없습니다.")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# 현재 활성화된 페이지 번호를 가져옴
|
# 현재 활성화된 페이지 번호를 가져옴
|
||||||
|
|
@ -588,14 +593,14 @@ class BrowserController:
|
||||||
if next_page_button:
|
if next_page_button:
|
||||||
await next_page_button.click() # 페이지 버튼 클릭
|
await next_page_button.click() # 페이지 버튼 클릭
|
||||||
# await self.page.wait_for_load_state('domcontentloaded') # 페이지 로딩이 완료될 때까지 대기
|
# await self.page.wait_for_load_state('domcontentloaded') # 페이지 로딩이 완료될 때까지 대기
|
||||||
self.logger.debug(f"페이지 {next_page_number}로 이동 완료.")
|
self.logger.info(f"페이지 {next_page_number}로 이동 완료.")
|
||||||
return True
|
return True
|
||||||
else:
|
else:
|
||||||
self.logger.debug("다음 페이지가 없습니다.")
|
self.logger.warning("다음 페이지가 없습니다.")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.logger.debug(f"다음 페이지로 이동 중 오류 발생: {e}", exc_info=True)
|
self.logger.error(f"다음 페이지로 이동 중 오류 발생: {e}", exc_info=True)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def switch_to_chrome(self):
|
def switch_to_chrome(self):
|
||||||
|
|
@ -608,9 +613,9 @@ class BrowserController:
|
||||||
win32gui.SetForegroundWindow(self.chrome_hwnd)
|
win32gui.SetForegroundWindow(self.chrome_hwnd)
|
||||||
self.logger.debug('크롬 창으로 포커스 이동.')
|
self.logger.debug('크롬 창으로 포커스 이동.')
|
||||||
else:
|
else:
|
||||||
self.logger.debug('크롬 창을 찾을 수 없습니다.')
|
self.logger.warning('크롬 창을 찾을 수 없습니다.')
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.logger.debug(f"크롬 포커스 전환 중 오류: {e}", exc_info=True)
|
self.logger.error(f"크롬 포커스 전환 중 오류: {e}", exc_info=True)
|
||||||
|
|
||||||
async def scroll_with_wheel(self, direction="down", pause_time=0.5, max_scrolls=50):
|
async def scroll_with_wheel(self, direction="down", pause_time=0.5, max_scrolls=50):
|
||||||
"""
|
"""
|
||||||
|
|
@ -734,7 +739,7 @@ class BrowserController:
|
||||||
|
|
||||||
async def scroll_page_to_bottom(self, pause_time=0.2):
|
async def scroll_page_to_bottom(self, pause_time=0.2):
|
||||||
"""페이지의 맨 아래까지 스크롤하여 모든 동적 요소를 로드"""
|
"""페이지의 맨 아래까지 스크롤하여 모든 동적 요소를 로드"""
|
||||||
self.logger.debug('페이지 스크롤 시작...')
|
self.logger.info('페이지 스크롤 시작...')
|
||||||
previous_height = await self.page.evaluate("() => document.body.scrollHeight")
|
previous_height = await self.page.evaluate("() => document.body.scrollHeight")
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
|
|
@ -744,11 +749,11 @@ class BrowserController:
|
||||||
if current_height == previous_height:
|
if current_height == previous_height:
|
||||||
break # 더 이상 스크롤할 내용이 없으면 종료
|
break # 더 이상 스크롤할 내용이 없으면 종료
|
||||||
previous_height = current_height
|
previous_height = current_height
|
||||||
self.logger.debug('페이지 스크롤 완료.')
|
self.logger.info('페이지 스크롤 완료.')
|
||||||
|
|
||||||
async def scroll_page_to_top(self, pause_time=0.2):
|
async def scroll_page_to_top(self, pause_time=0.2):
|
||||||
"""페이지의 맨 위까지 스크롤"""
|
"""페이지의 맨 위까지 스크롤"""
|
||||||
self.logger.debug('페이지 위로 스크롤 시작...')
|
self.logger.info('페이지 위로 스크롤 시작...')
|
||||||
previous_height = await self.page.evaluate("() => window.pageYOffset")
|
previous_height = await self.page.evaluate("() => window.pageYOffset")
|
||||||
|
|
||||||
while previous_height > 0:
|
while previous_height > 0:
|
||||||
|
|
@ -759,4 +764,4 @@ class BrowserController:
|
||||||
break # 더 이상 스크롤할 내용이 없으면 종료
|
break # 더 이상 스크롤할 내용이 없으면 종료
|
||||||
previous_height = current_height
|
previous_height = current_height
|
||||||
|
|
||||||
self.logger.debug('페이지 위로 스크롤 완료.')
|
self.logger.info('페이지 위로 스크롤 완료.')
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,11 @@ class ClipboardImageManager:
|
||||||
self.browser_controller = browser_controller # BrowserController 인스턴스를 전달받음
|
self.browser_controller = browser_controller # BrowserController 인스턴스를 전달받음
|
||||||
self.debug = debug # 디버그 플래그를 클래스 변수로 사용
|
self.debug = debug # 디버그 플래그를 클래스 변수로 사용
|
||||||
|
|
||||||
|
<<<<<<< HEAD
|
||||||
# self.debug = True
|
# self.debug = True
|
||||||
|
=======
|
||||||
|
self.debug = False
|
||||||
|
>>>>>>> f71275020a91aa828fd528172caa46a2bd494578
|
||||||
|
|
||||||
def get_clipboard_data(self):
|
def get_clipboard_data(self):
|
||||||
"""클립보드의 텍스트 또는 이미지 데이터를 가져옵니다."""
|
"""클립보드의 텍스트 또는 이미지 데이터를 가져옵니다."""
|
||||||
|
|
@ -144,7 +148,7 @@ class ClipboardImageManager:
|
||||||
self.logger.debug("유효하지 않은 Base64 이미지 데이터입니다.")
|
self.logger.debug("유효하지 않은 Base64 이미지 데이터입니다.")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
async def download_image_from_url(self, url, max_retries=3):
|
def download_image_from_url(self, url, max_retries=3):
|
||||||
"""URL에서 이미지를 다운로드하고 PIL 이미지 객체로 반환"""
|
"""URL에서 이미지를 다운로드하고 PIL 이미지 객체로 반환"""
|
||||||
headers = {
|
headers = {
|
||||||
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.150 Safari/537.36",
|
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.150 Safari/537.36",
|
||||||
|
|
@ -179,16 +183,19 @@ class ClipboardImageManager:
|
||||||
else:
|
else:
|
||||||
self.logger.debug(f"이미지 로딩 실패, HTTP 상태 코드: {response.status_code}. 재시도 {retries + 1}/{max_retries}")
|
self.logger.debug(f"이미지 로딩 실패, HTTP 상태 코드: {response.status_code}. 재시도 {retries + 1}/{max_retries}")
|
||||||
retries += 1
|
retries += 1
|
||||||
await asyncio.sleep(random.randint(2, 5)) # 2~5초 대기 후 재시도
|
# await asyncio.sleep(random.randint(2, 5)) # 2~5초 대기 후 재시도
|
||||||
|
time.sleep(random.randint(2, 5))
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.logger.debug(f"이미지 로딩 중 오류 발생: {e}. 재시도 {retries + 1}/{max_retries}")
|
self.logger.debug(f"이미지 로딩 중 오류 발생: {e}. 재시도 {retries + 1}/{max_retries}")
|
||||||
retries += 1
|
retries += 1
|
||||||
await asyncio.sleep(random.randint(2, 5)) # 예외 발생 시 대기 후 재시도
|
# await asyncio.sleep(random.randint(2, 5)) # 예외 발생 시 대기 후 재시도
|
||||||
|
time.sleep(random.randint(2, 5))
|
||||||
|
|
||||||
self.logger.debug("이미지 다운로드 최대 재시도 횟수를 초과했습니다.")
|
self.logger.debug("이미지 다운로드 최대 재시도 횟수를 초과했습니다.")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
async def process_clipboard(self, original_url, path=None):
|
def process_clipboard(self, original_url, path=None):
|
||||||
"""클립보드의 내용을 처리하고, 필요한 경우 이미지 변환, 크롭 또는 클립보드 비우기"""
|
"""클립보드의 내용을 처리하고, 필요한 경우 이미지 변환, 크롭 또는 클립보드 비우기"""
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
|
@ -248,7 +255,7 @@ class ClipboardImageManager:
|
||||||
self.logger.info("[process_clipboard] 클립보드에 이미지 없음")
|
self.logger.info("[process_clipboard] 클립보드에 이미지 없음")
|
||||||
|
|
||||||
if original_url:
|
if original_url:
|
||||||
image = await self.download_image_from_url(original_url)
|
image = self.download_image_from_url(original_url)
|
||||||
if image:
|
if image:
|
||||||
self.logger.debug("원본 이미지 다운로드 성공, 클립보드에 저장 중...")
|
self.logger.debug("원본 이미지 다운로드 성공, 클립보드에 저장 중...")
|
||||||
self.set_image_to_clipboard(image) # 크롭 없이 저장
|
self.set_image_to_clipboard(image) # 크롭 없이 저장
|
||||||
|
|
@ -266,7 +273,13 @@ class ClipboardImageManager:
|
||||||
|
|
||||||
def is_clipboard_image(self):
|
def is_clipboard_image(self):
|
||||||
"""클립보드에 이미지가 있는지 확인하는 함수"""
|
"""클립보드에 이미지가 있는지 확인하는 함수"""
|
||||||
return win32clipboard.IsClipboardFormatAvailable(win32clipboard.CF_DIB)
|
is_clipboard_image_flag = win32clipboard.IsClipboardFormatAvailable(win32clipboard.CF_DIB)
|
||||||
|
if is_clipboard_image_flag:
|
||||||
|
self.logger.debug("클립보드에 이미지가 존재합니다.")
|
||||||
|
else:
|
||||||
|
self.logger.debug("클립보드에 이미지가 없습니다.")
|
||||||
|
|
||||||
|
return is_clipboard_image_flag
|
||||||
|
|
||||||
def get_image_from_clipboard(self):
|
def get_image_from_clipboard(self):
|
||||||
"""클립보드에서 이미지를 가져오는 함수"""
|
"""클립보드에서 이미지를 가져오는 함수"""
|
||||||
|
|
|
||||||
16
config.ini
|
|
@ -7,7 +7,8 @@ oversea_shipping_locator = '//*[@id='productMainContentContainerId']/div/div[1]/
|
||||||
option_count_text_locator = 'div#productMainContentContainerId th:nth-child(2) > div > span'
|
option_count_text_locator = 'div#productMainContentContainerId th:nth-child(2) > div > span'
|
||||||
product_cost_locator = '//*[@id='productMainContentContainerId']/div/div[2]/div/div/div[5]/div[1]/div/div/div/div/div[2]/table/tbody/tr[{index}]/td[3]/div/div/div/div[2]/input'
|
product_cost_locator = '//*[@id='productMainContentContainerId']/div/div[2]/div/div/div[5]/div[1]/div/div/div/div/div[2]/table/tbody/tr[{index}]/td[3]/div/div/div/div[2]/input'
|
||||||
standard_selling_price_locator = '//*[@id='productMainContentContainerId']/div/div[2]/div/div/div[5]/div[1]/div/div/div/div/div[2]/table/tbody/tr[{index}]/td[4]/div/div/div[1]/div/div[2]/input'
|
standard_selling_price_locator = '//*[@id='productMainContentContainerId']/div/div[2]/div/div/div[5]/div[1]/div/div/div/div/div[2]/table/tbody/tr[{index}]/td[4]/div/div/div[1]/div/div[2]/input'
|
||||||
|
product_cost_for_single_locator = '//*[@id="productMainContentContainerId"]/div/div[2]/div/div/div[2]/div[1]/div/div/div/div/div[2]/table/tbody/tr[2]/td[2]/div/div/div/div[2]/input'
|
||||||
|
standard_selling_price_for_single_locator = '//*[@id="productMainContentContainerId"]/div/div[2]/div/div/div[2]/div[1]/div/div/div/div/div[2]/table/tbody/tr[2]/td[3]/div/div/div[1]/div/div[2]/input'
|
||||||
|
|
||||||
[OptionLocators]
|
[OptionLocators]
|
||||||
# 옵션 관련 선택자
|
# 옵션 관련 선택자
|
||||||
|
|
@ -18,18 +19,22 @@ option_product_locator = '//div[@id="productMainContentContainerId"]//label[cont
|
||||||
total_options_selector = '#productMainContentContainerId label.ant-checkbox-wrapper'
|
total_options_selector = '#productMainContentContainerId label.ant-checkbox-wrapper'
|
||||||
; is_all_option_checked_selector = '#productMainContentContainerId .ant-checkbox-indeterminate'
|
; is_all_option_checked_selector = '#productMainContentContainerId .ant-checkbox-indeterminate'
|
||||||
is_all_option_checked_selector = '//*[@id="productMainContentContainerId"]/div[1]/div[2]/div/div/div[2]/div/div[1]/div/div/div[2]/div/div/div[4]/div[2]/div[1]/label/span[1]/input'
|
is_all_option_checked_selector = '//*[@id="productMainContentContainerId"]/div[1]/div[2]/div/div/div[2]/div/div[1]/div/div/div[2]/div/div/div[4]/div[2]/div[1]/label/span[1]/input'
|
||||||
ai_option_btn_selector = 'div#productMainContentContainerId div:nth-child(2) > div > div > div.ant-row.ant-row-middle.css-1li46mu > div:nth-child(4) > button[type=\"button\"]'
|
; ai_option_btn_selector = 'div#productMainContentContainerId div:nth-child(2) > div > div > div.ant-row.ant-row-middle.css-1li46mu > div:nth-child(4) > button[type=\"button\"]'
|
||||||
|
ai_option_btn_selector = 'button:has-text("AI 옵션명 다듬기")'
|
||||||
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'
|
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'
|
||||||
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'
|
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'
|
||||||
checkbox_selector_template = '#productMainContentContainerId li:nth-child({index}) input[type="checkbox"]'
|
checkbox_selector_template = '#productMainContentContainerId li:nth-child({index}) input[type="checkbox"]'
|
||||||
image_selector_template = '#productMainContentContainerId li:nth-child({index}) img.sc-gbvfcU.ezktkd'
|
; image_selector_template = '#productMainContentContainerId li:nth-child({index}) img.sc-gbvfcU.ezktkd'
|
||||||
price_selector_template = '#productMainContentContainerId li:nth-child({index}) sup'
|
image_selector_template = 'div#productMainContentContainerId li:nth-child({index}) > div > div:nth-child(1) > div > div:nth-child(2) > div > img'
|
||||||
|
; price_selector_template = '#productMainContentContainerId li:nth-child({index}) sup'
|
||||||
|
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'
|
||||||
delete_button_selector_template = '#productMainContentContainerId > div.sc-TOgAA.fZvEqY > div:nth-child(2) > div > div > div:nth-child(2) > div > div.sc-cFShuL.dbIeho > div > div > div.ant-collapse-content.ant-collapse-content-active > div > div > div.sc-fGdiLE.iyXMeU > div.ant-list.ant-list-split.css-1li46mu > div > div > ul > li:nth-child({index}) > div > div:nth-child(1) > div > div:nth-child(2) > div > div.ant-row.ant-row-no-wrap.ant-row-space-between.ant-row-middle.css-1li46mu > div:nth-child(1) > div'
|
delete_button_selector_template = '#productMainContentContainerId > div.sc-TOgAA.fZvEqY > div:nth-child(2) > div > div > div:nth-child(2) > div > div.sc-cFShuL.dbIeho > div > div > div.ant-collapse-content.ant-collapse-content-active > div > div > div.sc-fGdiLE.iyXMeU > div.ant-list.ant-list-split.css-1li46mu > div > div > ul > li:nth-child({index}) > div > div:nth-child(1) > div > div:nth-child(2) > div > div.ant-row.ant-row-no-wrap.ant-row-space-between.ant-row-middle.css-1li46mu > div:nth-child(1) > div'
|
||||||
confirm_delete_button_locator = 'body > div:nth-child(18) > div > div.ant-modal-wrap.ant-modal-confirm-centered.ant-modal-centered > div > div.sc-ddjGPC.jbwEYW > div > div > div > div.ant-modal-confirm-btns > button.ant-btn.css-1li46mu.ant-btn-primary.ant-btn-dangerous'
|
confirm_delete_button_locator = 'body > div:nth-child(18) > div > div.ant-modal-wrap.ant-modal-confirm-centered.ant-modal-centered > div > div.sc-ddjGPC.jbwEYW > div > div > div > div.ant-modal-confirm-btns > button.ant-btn.css-1li46mu.ant-btn-primary.ant-btn-dangerous'
|
||||||
add_button_selector_template = '#productMainContentContainerId > div.sc-TOgAA.fZvEqY > div:nth-child(2) > div > div > div:nth-child(2) > div > div.sc-cFShuL.dbIeho > div > div > div.ant-collapse-content.ant-collapse-content-active > div > div > div.sc-fGdiLE.iyXMeU > div.ant-list.ant-list-split.css-1li46mu > div > div > ul > li:nth-child({index}) > div > div:nth-child(1) > div > div:nth-child(2) > div > div > img'
|
add_button_selector_template = '#productMainContentContainerId > div.sc-TOgAA.fZvEqY > div:nth-child(2) > div > div > div:nth-child(2) > div > div.sc-cFShuL.dbIeho > div > div > div.ant-collapse-content.ant-collapse-content-active > div > div > div.sc-fGdiLE.iyXMeU > div.ant-list.ant-list-split.css-1li46mu > div > div > ul > li:nth-child({index}) > div > div:nth-child(1) > div > div:nth-child(2) > div > div > img'
|
||||||
file_input_locator = 'input[type="file"]'
|
file_input_locator = 'input[type="file"]'
|
||||||
low_order_button_locator = 'button:has-text("가격 낮은 순")'
|
low_order_button_locator = 'button:has-text("가격 낮은 순")'
|
||||||
AtoZ_button_locator = 'button:has-text("A-Z")'
|
AtoZ_button_locator = 'button:has-text("A-Z")'
|
||||||
|
one_to_nine_button_locator = 'button:has-text("1-99")'
|
||||||
|
|
||||||
[DetailLocators]
|
[DetailLocators]
|
||||||
product_detail_input_locator = '//*[@id='detailMainContainerId']/div/div/div[{i}]/textarea'
|
product_detail_input_locator = '//*[@id='detailMainContainerId']/div/div/div[{i}]/textarea'
|
||||||
|
|
@ -95,7 +100,8 @@ staff_login_button_locator = 'button:has-text("직원 로그인 하기")'
|
||||||
admin_toggle_locator = 'button[role="switch"]'
|
admin_toggle_locator = 'button[role="switch"]'
|
||||||
|
|
||||||
# 광고 다이얼로그 관련 선택자
|
# 광고 다이얼로그 관련 선택자
|
||||||
close_ad_dialog_locator = 'div.ant-modal-wrap.ant-modal-centered'
|
; close_ad_dialog_locator = 'div.ant-modal-wrap.ant-modal-centered'
|
||||||
|
close_ad_dialog_locator = 'div[role="dialog"]'
|
||||||
close_ad_button_locator = 'div.ant-modal-footer > div > div > button[type='button'].ant-btn.css-1li46mu.ant-btn-default'
|
close_ad_button_locator = 'div.ant-modal-footer > div > div > button[type='button'].ant-btn.css-1li46mu.ant-btn-default'
|
||||||
|
|
||||||
# 상품 관련 선택자
|
# 상품 관련 선택자
|
||||||
|
|
|
||||||
22
gui.py
|
|
@ -56,7 +56,7 @@ class TranslationApp(QWidget):
|
||||||
self.cmb_diag = CMBSettingsDialog(parent=self, logger=self.logger, db_manager=self.db_manager, initial_db_path=self.initial_db_path, user_db_path=self.user_db_path, debug=self.debug)
|
self.cmb_diag = CMBSettingsDialog(parent=self, logger=self.logger, db_manager=self.db_manager, initial_db_path=self.initial_db_path, user_db_path=self.user_db_path, debug=self.debug)
|
||||||
self.clipboardImageManager = ClipboardImageManager(self, logger, self.browser_controller, self.debug)
|
self.clipboardImageManager = ClipboardImageManager(self, logger, self.browser_controller, self.debug)
|
||||||
self.optionHandler = OptionHandler(self.locator_manager, self.browser_controller, self.whale_translator, self.logger, self.vertexAI, self.debug)
|
self.optionHandler = OptionHandler(self.locator_manager, self.browser_controller, self.whale_translator, self.logger, self.vertexAI, self.debug)
|
||||||
self.priceHandler = PriceHandler(self.locator_manager, self.browser_controller, self.logger, self.vertexAI, self.cmb_diag, self.debug)
|
self.priceHandler = PriceHandler(self.locator_manager, self.browser_controller, self.logger, self.optionHandler, self.vertexAI, self.cmb_diag, self.debug)
|
||||||
self.titleHandler = TitleHandler(self.locator_manager, self.browser_controller, self.logger)
|
self.titleHandler = TitleHandler(self.locator_manager, self.browser_controller, self.logger)
|
||||||
self.running = False
|
self.running = False
|
||||||
|
|
||||||
|
|
@ -72,6 +72,7 @@ class TranslationApp(QWidget):
|
||||||
self.price_count = 0
|
self.price_count = 0
|
||||||
self.detail_image_count = 0
|
self.detail_image_count = 0
|
||||||
self.thumb_image_count = 0
|
self.thumb_image_count = 0
|
||||||
|
self.current_options_info = {}
|
||||||
|
|
||||||
self.current_stage_index = 0 # 현재 진행 중인 단계 인덱스
|
self.current_stage_index = 0 # 현재 진행 중인 단계 인덱스
|
||||||
|
|
||||||
|
|
@ -715,7 +716,7 @@ class TranslationApp(QWidget):
|
||||||
self.logger.debug(f'현재 페이지: {page_number}')
|
self.logger.debug(f'현재 페이지: {page_number}')
|
||||||
|
|
||||||
if not page_number == 1:
|
if not page_number == 1:
|
||||||
self.browser_controller.scroll_page_to_top()
|
await self.browser_controller.scroll_page_to_top()
|
||||||
self.logger.debug(f'1페이지가 아니므로 동적로딩을 위해 휠 스크롤 업')
|
self.logger.debug(f'1페이지가 아니므로 동적로딩을 위해 휠 스크롤 업')
|
||||||
|
|
||||||
# 4. 현재 페이지의 모든 "세부사항 수정 및 업로드" 버튼 찾기
|
# 4. 현재 페이지의 모든 "세부사항 수정 및 업로드" 버튼 찾기
|
||||||
|
|
@ -842,10 +843,16 @@ class TranslationApp(QWidget):
|
||||||
self.logger.debug('번역 작업이 중단되었습니다.')
|
self.logger.debug('번역 작업이 중단되었습니다.')
|
||||||
break
|
break
|
||||||
|
|
||||||
self.logger.debug(f"이미지 번역 프로세스")
|
self.logger.debug(f"웨일 브라우저를 활용한 이미지 번역 프로세스")
|
||||||
self.whale_translator.translate_image(url)
|
is_success_translated = self.whale_translator.translate_image(url)
|
||||||
self.logger.debug(f"이미지 붙여넣기")
|
|
||||||
await self.browser_controller.paste_image_in_chrome(self.clipboardImageManager, url)
|
if is_success_translated:
|
||||||
|
self.logger.debug(f"paste_image_in_chrome - 이미지 붙여넣기")
|
||||||
|
is_paste_success = self.browser_controller.paste_image_in_chrome(self.clipboardImageManager, url)
|
||||||
|
else:
|
||||||
|
self.logger.debug(f"{url} gui 이미지 번역 실패")
|
||||||
|
is_paste_success = False
|
||||||
|
|
||||||
self.logger.debug(f"Progress Update")
|
self.logger.debug(f"Progress Update")
|
||||||
self.update_detail_progress(i,total_images)
|
self.update_detail_progress(i,total_images)
|
||||||
|
|
||||||
|
|
@ -866,8 +873,7 @@ class TranslationApp(QWidget):
|
||||||
|
|
||||||
# 옵션 최대선택갯수
|
# 옵션 최대선택갯수
|
||||||
max_option_count = 20
|
max_option_count = 20
|
||||||
option_image_trans = False
|
self.current_options_info = await self.optionHandler.process_options(product_name, max_option_count, self.toggle_states)
|
||||||
await self.optionHandler.process_options(product_name, max_option_count, self.toggle_states)
|
|
||||||
|
|
||||||
# 수정 후 저장
|
# 수정 후 저장
|
||||||
# await self.optionHandler.save_option()
|
# await self.optionHandler.save_option()
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,8 @@ class LocatorManager:
|
||||||
'option_count_text_locator': self.config.get('PriceLocators', 'option_count_text_locator').strip("'"),
|
'option_count_text_locator': self.config.get('PriceLocators', 'option_count_text_locator').strip("'"),
|
||||||
'product_cost_locator': self.config.get('PriceLocators', 'product_cost_locator').strip("'"),
|
'product_cost_locator': self.config.get('PriceLocators', 'product_cost_locator').strip("'"),
|
||||||
'standard_selling_price_locator': self.config.get('PriceLocators', 'standard_selling_price_locator').strip("'"),
|
'standard_selling_price_locator': self.config.get('PriceLocators', 'standard_selling_price_locator').strip("'"),
|
||||||
|
'product_cost_for_single_locator': self.config.get('PriceLocators', 'product_cost_for_single_locator').strip("'"),
|
||||||
|
'standard_selling_price_for_single_locator': self.config.get('PriceLocators', 'standard_selling_price_for_single_locator').strip("'"),
|
||||||
}
|
}
|
||||||
|
|
||||||
# BrowserControl 섹션
|
# BrowserControl 섹션
|
||||||
|
|
@ -83,6 +85,7 @@ class LocatorManager:
|
||||||
'file_input_locator': self.config.get('OptionLocators', 'file_input_locator').strip("'"),
|
'file_input_locator': self.config.get('OptionLocators', 'file_input_locator').strip("'"),
|
||||||
'low_order_button_locator': self.config.get('OptionLocators', 'low_order_button_locator').strip("'"),
|
'low_order_button_locator': self.config.get('OptionLocators', 'low_order_button_locator').strip("'"),
|
||||||
'AtoZ_button_locator': self.config.get('OptionLocators', 'AtoZ_button_locator').strip("'"),
|
'AtoZ_button_locator': self.config.get('OptionLocators', 'AtoZ_button_locator').strip("'"),
|
||||||
|
'one_to_nine_button_locator': self.config.get('OptionLocators', 'one_to_nine_button_locator').strip("'"),
|
||||||
}
|
}
|
||||||
|
|
||||||
# TitleLocators 섹션
|
# TitleLocators 섹션
|
||||||
|
|
|
||||||
102
option.py
|
|
@ -13,6 +13,8 @@ class OptionHandler:
|
||||||
self.debug_flag = debug_flag
|
self.debug_flag = debug_flag
|
||||||
self.vertexAItranslator = vertexAI
|
self.vertexAItranslator = vertexAI
|
||||||
self.whale_translator = whale_translator
|
self.whale_translator = whale_translator
|
||||||
|
self.is_percenty_success = False
|
||||||
|
self.is_vertext_success = False
|
||||||
self.init_option_info()
|
self.init_option_info()
|
||||||
|
|
||||||
# 선택자 로드
|
# 선택자 로드
|
||||||
|
|
@ -34,6 +36,7 @@ class OptionHandler:
|
||||||
self.file_input_locator = self.locator_manager.get_locator('OptionLocators', 'file_input_locator')
|
self.file_input_locator = self.locator_manager.get_locator('OptionLocators', 'file_input_locator')
|
||||||
self.low_order_button_locator = self.locator_manager.get_locator('OptionLocators', 'low_order_button_locator')
|
self.low_order_button_locator = self.locator_manager.get_locator('OptionLocators', 'low_order_button_locator')
|
||||||
self.AtoZ_button_locator = self.locator_manager.get_locator('OptionLocators', 'AtoZ_button_locator')
|
self.AtoZ_button_locator = self.locator_manager.get_locator('OptionLocators', 'AtoZ_button_locator')
|
||||||
|
self.one_to_nine_button_locator = self.locator_manager.get_locator('OptionLocators', 'one_to_nine_button_locator')
|
||||||
|
|
||||||
def update_page(self, page1):
|
def update_page(self, page1):
|
||||||
self.page = page1
|
self.page = page1
|
||||||
|
|
@ -41,6 +44,8 @@ class OptionHandler:
|
||||||
|
|
||||||
def init_option_info(self):
|
def init_option_info(self):
|
||||||
self.option_info = {
|
self.option_info = {
|
||||||
|
'is_single_option': False,
|
||||||
|
'is_completed_option': False,
|
||||||
'original_names': {},
|
'original_names': {},
|
||||||
'translated_names': {},
|
'translated_names': {},
|
||||||
'selected_translated_options': {},
|
'selected_translated_options': {},
|
||||||
|
|
@ -55,6 +60,7 @@ class OptionHandler:
|
||||||
"""클래스 변수에 저장된 선택된 번역된 옵션들을 반환"""
|
"""클래스 변수에 저장된 선택된 번역된 옵션들을 반환"""
|
||||||
return self.option_info.get('selected_translated_options', [])
|
return self.option_info.get('selected_translated_options', [])
|
||||||
|
|
||||||
|
|
||||||
def filter_bait_items_with_price_distribution(self, options, lower_z=-0.9, upper_z=1.5, base_ratio=2):
|
def filter_bait_items_with_price_distribution(self, options, lower_z=-0.9, upper_z=1.5, base_ratio=2):
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
@ -116,10 +122,14 @@ class OptionHandler:
|
||||||
async def store_selected_options(self):
|
async def store_selected_options(self):
|
||||||
"""현재 페이지에서 선택된 옵션을 수집하여, 가격 낮은 순으로 정렬한 후 클래스 변수에 저장"""
|
"""현재 페이지에서 선택된 옵션을 수집하여, 가격 낮은 순으로 정렬한 후 클래스 변수에 저장"""
|
||||||
try:
|
try:
|
||||||
selected_translated_options = []
|
selected_translated_options = {}
|
||||||
|
|
||||||
total_options_count = len(self.option_info['original_names'])
|
total_options_count = len(self.option_info['original_names'])
|
||||||
|
|
||||||
|
# checked_states 딕셔너리에서 값이 True인 항목의 개수를 계산
|
||||||
|
checked_count = sum(value is True for value in self.option_info['checked_states'].values())
|
||||||
|
self.logger.debug(f"체크된 옵션 수: {checked_count}")
|
||||||
|
|
||||||
await self.low_order_click()
|
await self.low_order_click()
|
||||||
|
|
||||||
for i in range(1, total_options_count + 1):
|
for i in range(1, total_options_count + 1):
|
||||||
|
|
@ -131,10 +141,7 @@ class OptionHandler:
|
||||||
option_input_element = await self.page.query_selector(option_input_selector)
|
option_input_element = await self.page.query_selector(option_input_selector)
|
||||||
if option_input_element:
|
if option_input_element:
|
||||||
option_name_value = (await option_input_element.get_attribute('value')).strip()
|
option_name_value = (await option_input_element.get_attribute('value')).strip()
|
||||||
selected_translated_options.append(
|
selected_translated_options[option_name_value] = self.option_info['prices'].get(option_name_value, {}).get('low_price', 0)
|
||||||
# (option_name_value, self.option_info['prices'].get(option_name_value, {}).get('low_price', 0))
|
|
||||||
(option_name_value)
|
|
||||||
)
|
|
||||||
|
|
||||||
self.option_info['selected_translated_options'] = selected_translated_options
|
self.option_info['selected_translated_options'] = selected_translated_options
|
||||||
self.logger.debug(f"선택된 옵션 저장 완료: {selected_translated_options}")
|
self.logger.debug(f"선택된 옵션 저장 완료: {selected_translated_options}")
|
||||||
|
|
@ -148,11 +155,14 @@ class OptionHandler:
|
||||||
옵션 처리 로직. 옵션을 번역하고 이미지를 업데이트함.
|
옵션 처리 로직. 옵션을 번역하고 이미지를 업데이트함.
|
||||||
|
|
||||||
:param product_name: 상품명 (str 형태).
|
:param product_name: 상품명 (str 형태).
|
||||||
:param max_option_count: 최대 옵션 갯수 (기본값 10).
|
:param max_option_count: 최대 옵션 갯수 (기본값 20).
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
self.logger.debug(f"상품명: {product_name}에 대한 옵션을 처리 중...")
|
self.logger.debug(f"상품명: {product_name}에 대한 옵션을 처리 중...")
|
||||||
|
|
||||||
|
self.logger.debug(f"이전 상품의 옵션정보를 초기화합니다.")
|
||||||
|
self.init_option_info()
|
||||||
|
|
||||||
# self.logger.debug(f"포커스를 위해 클릭1회")
|
# self.logger.debug(f"포커스를 위해 클릭1회")
|
||||||
# self.low_order_click()
|
# self.low_order_click()
|
||||||
|
|
||||||
|
|
@ -168,15 +178,23 @@ class OptionHandler:
|
||||||
# 1. 단일 옵션인지 판단
|
# 1. 단일 옵션인지 판단
|
||||||
if await self.is_single_option():
|
if await self.is_single_option():
|
||||||
self.logger.debug("단일 옵션 상품입니다. 옵션 수정 과정을 생략합니다.")
|
self.logger.debug("단일 옵션 상품입니다. 옵션 수정 과정을 생략합니다.")
|
||||||
return
|
self.option_info['is_single_option'] = True
|
||||||
|
|
||||||
|
return self.option_info
|
||||||
|
|
||||||
# 2. 전체 옵션 체크박스 상태 확인
|
# 2. 전체 옵션 체크박스 상태 확인
|
||||||
click_to_check_to_all = True
|
click_to_check_to_all = True
|
||||||
self.logger.debug(f"언제나 전체체크에서 시작 - {click_to_check_to_all}")
|
|
||||||
|
|
||||||
if not await self.is_all_options_checked(click_to_check_to_all):
|
self.logger.debug(f"일부 체크된 옵션상품에 대한 처리 방법 : {'전체체크에서 시작' if click_to_check_to_all else '일부 체크시 수정완료로 판단'}")
|
||||||
|
|
||||||
|
if click_to_check_to_all:
|
||||||
|
self.logger.debug("옵션이 일부만 체크된 상태입니다. 전체 체크로 바꿉니다.")
|
||||||
|
await self.is_all_options_checked(click_to_check_to_all)
|
||||||
|
else:
|
||||||
self.logger.debug("옵션이 일부만 체크된 상태입니다. 옵션 수정이 완료된 상품으로 판단하여 패스합니다.")
|
self.logger.debug("옵션이 일부만 체크된 상태입니다. 옵션 수정이 완료된 상품으로 판단하여 패스합니다.")
|
||||||
return
|
self.option_info['is_completed_option'] = True
|
||||||
|
|
||||||
|
return self.option_info
|
||||||
|
|
||||||
# 3. 가격 낮은 순 정렬 클릭
|
# 3. 가격 낮은 순 정렬 클릭
|
||||||
await self.low_order_click()
|
await self.low_order_click()
|
||||||
|
|
@ -187,15 +205,13 @@ class OptionHandler:
|
||||||
self.logger.debug(f"옵션 AI번역 : {toggle_states['optionTrnas']}")
|
self.logger.debug(f"옵션 AI번역 : {toggle_states['optionTrnas']}")
|
||||||
self.option_info = await self.collect_options_info()
|
self.option_info = await self.collect_options_info()
|
||||||
|
|
||||||
translation_success = False # 성공/실패 플래그
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Vertex AI를 통한 번역 시도
|
# Vertex AI를 통한 번역 시도
|
||||||
translated_options = await self.vertexAItranslator.translate_options(self.option_info['original_names'], product_name)
|
translated_options = await self.vertexAItranslator.translate_options(self.option_info['original_names'], product_name)
|
||||||
self.logger.debug(f"번역된 옵션 입력")
|
self.logger.debug(f"번역된 옵션 입력")
|
||||||
await self.apply_translated_options(translated_options, self.option_info['edit_fields'])
|
await self.apply_translated_options(translated_options, self.option_info['edit_fields'])
|
||||||
|
|
||||||
translation_success = True # 번역 성공
|
self.is_vertext_success = True # 번역 성공
|
||||||
|
|
||||||
except ValueError as ve:
|
except ValueError as ve:
|
||||||
# 안전 필터 예외 처리
|
# 안전 필터 예외 처리
|
||||||
|
|
@ -205,7 +221,8 @@ class OptionHandler:
|
||||||
await self.page.click(self.ai_option_btn_selector)
|
await self.page.click(self.ai_option_btn_selector)
|
||||||
self.logger.debug("번역을 위한 5초간 대기")
|
self.logger.debug("번역을 위한 5초간 대기")
|
||||||
await asyncio.sleep(5)
|
await asyncio.sleep(5)
|
||||||
translation_success = False # 번역 실패
|
self.is_vertext_success = False
|
||||||
|
self.is_percenty_success = True
|
||||||
|
|
||||||
# except google.api_core.exceptions.ResourceExhausted as re:
|
# except google.api_core.exceptions.ResourceExhausted as re:
|
||||||
# # 할당량 초과 예외 처리
|
# # 할당량 초과 예외 처리
|
||||||
|
|
@ -223,10 +240,11 @@ class OptionHandler:
|
||||||
await self.page.click(self.ai_option_btn_selector)
|
await self.page.click(self.ai_option_btn_selector)
|
||||||
self.logger.debug("번역을 위한 5초간 대기")
|
self.logger.debug("번역을 위한 5초간 대기")
|
||||||
await asyncio.sleep(5)
|
await asyncio.sleep(5)
|
||||||
translation_success = False # 번역 실패
|
self.is_vertext_success = False # 번역 실패
|
||||||
|
self.is_percenty_success = True
|
||||||
|
|
||||||
# 번역 성공 여부에 따른 로그
|
# 번역 성공 여부에 따른 로그
|
||||||
self.logger.debug(f"[{'VertexAI' if translation_success else '퍼센티AI'}] 를 이용한 옵션번역 성공")
|
self.logger.debug(f"[{'VertexAI' if self.is_vertext_success else '퍼센티AI'}] 를 이용한 옵션번역 성공")
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
# 옵션 처리 중 오류 발생 시 전체 로그 출력
|
# 옵션 처리 중 오류 발생 시 전체 로그 출력
|
||||||
|
|
@ -237,8 +255,9 @@ class OptionHandler:
|
||||||
self.logger.debug(f"옵션 필터링 및 조정 : {toggle_states['optionAutoSelect']}")
|
self.logger.debug(f"옵션 필터링 및 조정 : {toggle_states['optionAutoSelect']}")
|
||||||
await self.filter_and_adjust_options(max_option_count)
|
await self.filter_and_adjust_options(max_option_count)
|
||||||
|
|
||||||
# 6. 선택된 옵션 재수집
|
# 6. 선택된 옵션정보 재수집
|
||||||
self.logger.debug(f"옵션 필터링 및 조정")
|
if self.is_percenty_success:
|
||||||
|
self.logger.debug(f"퍼센티 옵션번역으로 인해 선택된 옵션정보 재수집")
|
||||||
await self.store_selected_options() # 페이지에서 실제 선택된 옵션을 수집하여 저장
|
await self.store_selected_options() # 페이지에서 실제 선택된 옵션을 수집하여 저장
|
||||||
|
|
||||||
# 7. 옵션 이미지 업데이트 (옵션 이미지가 있는 경우)
|
# 7. 옵션 이미지 업데이트 (옵션 이미지가 있는 경우)
|
||||||
|
|
@ -248,39 +267,26 @@ class OptionHandler:
|
||||||
option_name = translated_options.get(f'trans_option_{index}', f'옵션_{index}')
|
option_name = translated_options.get(f'trans_option_{index}', f'옵션_{index}')
|
||||||
await self.update_option_image(index, option_image_url, product_name, option_name, self.debug_flag)
|
await self.update_option_image(index, option_image_url, product_name, option_name, self.debug_flag)
|
||||||
|
|
||||||
# # Vertex AI를 통해 옵션명을 번역
|
# 8. A-Z or 1-99 button 클릭
|
||||||
# translated_options = await self.vertexAItranslator.translate_options(self.option_info['original_names'], product_name)
|
|
||||||
# self.logger.debug(f"번역된 옵션: {translated_options}")
|
|
||||||
|
|
||||||
# # 5. 번역된 옵션명 편집칸에 입력
|
what_prefix_button = '1-99'
|
||||||
# self.logger.debug("번역된 옵션명을 입력합니다.")
|
|
||||||
# await self.apply_translated_options(translated_options, self.option_info['edit_fields'])
|
|
||||||
|
|
||||||
# # 6. 옵션 이미지 업데이트 (옵션 이미지가 있는 경우)
|
if what_prefix_button == 'A-Z':
|
||||||
# self.logger.debug("옵션 이미지 업데이트 (옵션 이미지가 있는 경우)")
|
await self.AtoZ_button_click()
|
||||||
# for index, option_image_url in enumerate(self.option_info.get('option_images', []), start=1):
|
elif what_prefix_button == '1-99':
|
||||||
# option_name = translated_options.get(f'trans_option_{index}', f'옵션_{index}')
|
# # 9. A-Z 버튼 클릭
|
||||||
# await self.update_option_image(index, option_image_url, product_name, option_name, self.debug)
|
await self.one_to_nine_button_click()
|
||||||
|
|
||||||
# # 7. 옵션 선택 및 제한 처리
|
|
||||||
# await self.adjust_options(self.option_info['checkboxes'], max_option_count)
|
|
||||||
|
|
||||||
# # 8. 정리된 옵션을 다시한번 더 가격 낮은 순으로 정렬 클릭
|
|
||||||
# await self.low_order_click()
|
|
||||||
|
|
||||||
# 9. A-Z 버튼 클릭
|
|
||||||
self.logger.debug("A-Z 버튼을 클릭합니다.")
|
|
||||||
self.AtoZ_button_click()
|
|
||||||
|
|
||||||
# 9. 저장 버튼 클릭
|
# 9. 저장 버튼 클릭
|
||||||
self.logger.debug("저장 버튼을 클릭합니다.")
|
self.logger.debug("저장 버튼을 클릭합니다.")
|
||||||
await self.page.click('button:has-text("저장하기")')
|
await self.page.click('button:has-text("저장하기")')
|
||||||
|
|
||||||
self.logger.debug("옵션 처리 완료.")
|
self.logger.debug("옵션 처리 완료.")
|
||||||
|
return self.option_info
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.logger.debug(f"옵션 처리 중 오류 발생: {e}", exc_info=True)
|
self.logger.debug(f"옵션 처리 중 오류 발생: {e}", exc_info=True)
|
||||||
return
|
return None
|
||||||
|
|
||||||
async def is_single_option(self):
|
async def is_single_option(self):
|
||||||
"""단일 상품 상태 여부를 확인하는 메서드"""
|
"""단일 상품 상태 여부를 확인하는 메서드"""
|
||||||
|
|
@ -347,7 +353,6 @@ class OptionHandler:
|
||||||
|
|
||||||
async def collect_options_info(self):
|
async def collect_options_info(self):
|
||||||
"""옵션 정보를 수집 (이미지, 옵션명, 편집 필드, 가격, 체크박스 정보 포함)"""
|
"""옵션 정보를 수집 (이미지, 옵션명, 편집 필드, 가격, 체크박스 정보 포함)"""
|
||||||
self.init_option_info()
|
|
||||||
try:
|
try:
|
||||||
# 총 옵션 갯수 수집
|
# 총 옵션 갯수 수집
|
||||||
total_options_element = await self.page.query_selector(self.total_options_selector)
|
total_options_element = await self.page.query_selector(self.total_options_selector)
|
||||||
|
|
@ -375,6 +380,12 @@ class OptionHandler:
|
||||||
elements = await asyncio.gather(*tasks)
|
elements = await asyncio.gather(*tasks)
|
||||||
original_name_element, edit_field_element, checkbox_element, image_element, price_element = elements
|
original_name_element, edit_field_element, checkbox_element, image_element, price_element = elements
|
||||||
|
|
||||||
|
self.logger.debug(f"{i}번째 original_name_element : {original_name_element}")
|
||||||
|
self.logger.debug(f"{i}번째 edit_field_element : {edit_field_element}")
|
||||||
|
self.logger.debug(f"{i}번째 checkbox_element : {checkbox_element}")
|
||||||
|
self.logger.debug(f"{i}번째 image_element : {image_element}")
|
||||||
|
self.logger.debug(f"{i}번째 price_element : {price_element}")
|
||||||
|
|
||||||
if original_name_element:
|
if original_name_element:
|
||||||
original_name = await original_name_element.inner_text()
|
original_name = await original_name_element.inner_text()
|
||||||
self.option_info['original_names'][f'origin_option_{i}'] = original_name
|
self.option_info['original_names'][f'origin_option_{i}'] = original_name
|
||||||
|
|
@ -383,6 +394,7 @@ class OptionHandler:
|
||||||
|
|
||||||
# 체크박스 상태 수집 (체크 여부는 클래스 이름으로 판단)
|
# 체크박스 상태 수집 (체크 여부는 클래스 이름으로 판단)
|
||||||
checkbox_state = None
|
checkbox_state = None
|
||||||
|
|
||||||
if checkbox_element:
|
if checkbox_element:
|
||||||
checkbox_classes = await checkbox_element.get_attribute('class')
|
checkbox_classes = await checkbox_element.get_attribute('class')
|
||||||
if 'ant-checkbox-checked' in checkbox_classes:
|
if 'ant-checkbox-checked' in checkbox_classes:
|
||||||
|
|
@ -633,6 +645,10 @@ class OptionHandler:
|
||||||
self.logger.debug("A-Z 버튼을 클릭합니다.")
|
self.logger.debug("A-Z 버튼을 클릭합니다.")
|
||||||
await self.page.click(self.AtoZ_button_locator)
|
await self.page.click(self.AtoZ_button_locator)
|
||||||
|
|
||||||
|
async def one_to_nine_button_click(self):
|
||||||
|
self.logger.debug("1-99 버튼을 클릭합니다.")
|
||||||
|
await self.page.click(self.one_to_nine_button_locator)
|
||||||
|
|
||||||
async def low_order_click(self):
|
async def low_order_click(self):
|
||||||
self.logger.debug("가격 낮은 순 정렬을 클릭합니다.")
|
self.logger.debug("가격 낮은 순 정렬을 클릭합니다.")
|
||||||
await self.page.click(self.low_order_button_locator)
|
await self.page.click(self.low_order_button_locator)
|
||||||
|
|
@ -696,7 +712,7 @@ class OptionHandler:
|
||||||
await add_button.click()
|
await add_button.click()
|
||||||
|
|
||||||
# 파일 선택 다이얼로그에서 번역된 이미지 파일 입력
|
# 파일 선택 다이얼로그에서 번역된 이미지 파일 입력
|
||||||
file_input = await self.page.wait_for_selector(file_input_locator)
|
file_input = await self.page.wait_for_selector(file_input_locator, timeout=5000)
|
||||||
await file_input.set_input_files(translated_image_path)
|
await file_input.set_input_files(translated_image_path)
|
||||||
self.logger.debug(f"{index}번째 옵션에 번역된 이미지가 추가되었습니다.")
|
self.logger.debug(f"{index}번째 옵션에 번역된 이미지가 추가되었습니다.")
|
||||||
|
|
||||||
|
|
@ -720,7 +736,7 @@ class OptionHandler:
|
||||||
original_image.save(original_image_path)
|
original_image.save(original_image_path)
|
||||||
|
|
||||||
# 저장된 원본 이미지를 다시 업로드
|
# 저장된 원본 이미지를 다시 업로드
|
||||||
file_input = await self.page.wait_for_selector(file_input_locator)
|
file_input = await self.page.wait_for_selector(file_input_locator, timeout=5000)
|
||||||
await file_input.set_input_files(original_image_path)
|
await file_input.set_input_files(original_image_path)
|
||||||
self.logger.debug(f"{index}번째 옵션에 원본 이미지가 업로드되었습니다.")
|
self.logger.debug(f"{index}번째 옵션에 원본 이미지가 업로드되었습니다.")
|
||||||
else:
|
else:
|
||||||
|
|
|
||||||
55
price.py
|
|
@ -7,9 +7,10 @@ import math , re, time
|
||||||
from playwright.async_api import TimeoutError
|
from playwright.async_api import TimeoutError
|
||||||
|
|
||||||
class PriceHandler:
|
class PriceHandler:
|
||||||
def __init__(self, locator_manager, browser_controller, logger, vertexAI, cmb_diag, debug_flag=False):
|
def __init__(self, locator_manager, browser_controller, logger, optionHandler,vertexAI, cmb_diag, debug_flag=False):
|
||||||
self.locator_manager = locator_manager
|
self.locator_manager = locator_manager
|
||||||
self.browser_controller = browser_controller
|
self.browser_controller = browser_controller
|
||||||
|
self.optionHandler = optionHandler
|
||||||
self.page = self.browser_controller.page
|
self.page = self.browser_controller.page
|
||||||
self.logger = logger
|
self.logger = logger
|
||||||
self.debug_flag = debug_flag
|
self.debug_flag = debug_flag
|
||||||
|
|
@ -27,10 +28,13 @@ class PriceHandler:
|
||||||
self.first_delv_fee = 0
|
self.first_delv_fee = 0
|
||||||
self.exchange_fee = 0
|
self.exchange_fee = 0
|
||||||
self.sold_price = 0
|
self.sold_price = 0
|
||||||
|
self.initail_margin_cost = 0
|
||||||
|
|
||||||
# Locator들을 미리 로드하여 초기화 - locator_template:동적 로딩, locator:고정로딩
|
# Locator들을 미리 로드하여 초기화 - locator_template:동적 로딩, locator:고정로딩
|
||||||
self.product_cost_locator_template = self.locator_manager.get_locator('PriceLocators', 'product_cost_locator')
|
self.product_cost_locator_template = self.locator_manager.get_locator('PriceLocators', 'product_cost_locator')
|
||||||
self.standard_selling_price_locator_template = self.locator_manager.get_locator('PriceLocators', 'standard_selling_price_locator')
|
self.standard_selling_price_locator_template = self.locator_manager.get_locator('PriceLocators', 'standard_selling_price_locator')
|
||||||
|
self.product_cost_for_single_locator = self.locator_manager.get_locator('PriceLocators', 'product_cost_for_single_locator')
|
||||||
|
self.standard_selling_price_for_single_locator = self.locator_manager.get_locator('PriceLocators', 'standard_selling_price_for_single_locator')
|
||||||
self.plus_margin_locator = self.locator_manager.get_locator('PriceLocators', 'plus_margin_locator')
|
self.plus_margin_locator = self.locator_manager.get_locator('PriceLocators', 'plus_margin_locator')
|
||||||
self.oversea_shipping_locator = self.locator_manager.get_locator('PriceLocators', 'oversea_shipping_locator')
|
self.oversea_shipping_locator = self.locator_manager.get_locator('PriceLocators', 'oversea_shipping_locator')
|
||||||
self.return_fee_input_locator = self.locator_manager.get_locator('PriceLocators', 'return_fee_input_locator')
|
self.return_fee_input_locator = self.locator_manager.get_locator('PriceLocators', 'return_fee_input_locator')
|
||||||
|
|
@ -86,7 +90,10 @@ class PriceHandler:
|
||||||
self.logger.debug(f"더하기마진값{initial_plusmargin}을 팔린가격{sold_price}으로 간주")
|
self.logger.debug(f"더하기마진값{initial_plusmargin}을 팔린가격{sold_price}으로 간주")
|
||||||
|
|
||||||
self.logger.debug("옵션 가격 정보를 수집합니다.")
|
self.logger.debug("옵션 가격 정보를 수집합니다.")
|
||||||
option_data, min_cost, max_cost, avg_cost, upper_avg_cost = await self.collect_product_costs_and_prices() # 수집된 옵션정보를 반환
|
|
||||||
|
is_single = self.optionHandler.option_info['is_single_option']
|
||||||
|
|
||||||
|
option_data, min_cost, max_cost, avg_cost, upper_avg_cost = await self.collect_product_costs_and_prices(is_single) # 수집된 옵션정보를 반환
|
||||||
if option_data is None:
|
if option_data is None:
|
||||||
self.logger.error("상품 옵션 정보를 수집하지 못했습니다.", exc_info=True)
|
self.logger.error("상품 옵션 정보를 수집하지 못했습니다.", exc_info=True)
|
||||||
return
|
return
|
||||||
|
|
@ -149,7 +156,7 @@ class PriceHandler:
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
# 반품비 수정
|
# 반품비 수정
|
||||||
return_fee_element = await self.page.wait_for_selector(self.return_fee_input_locator)
|
return_fee_element = await self.page.wait_for_selector(self.return_fee_input_locator, timeout=5000)
|
||||||
if return_fee_element:
|
if return_fee_element:
|
||||||
return_fee = self.round_to_UP(return_fee)
|
return_fee = self.round_to_UP(return_fee)
|
||||||
await return_fee_element.fill(str(return_fee)) # 기존 내용을 지우고 새 값을 입력
|
await return_fee_element.fill(str(return_fee)) # 기존 내용을 지우고 새 값을 입력
|
||||||
|
|
@ -158,7 +165,7 @@ class PriceHandler:
|
||||||
self.logger.error(f"반품비 입력 중 오류 발생: 요소를 찾을 수 없음", exc_info=True)
|
self.logger.error(f"반품비 입력 중 오류 발생: 요소를 찾을 수 없음", exc_info=True)
|
||||||
|
|
||||||
# 초도배송비 수정
|
# 초도배송비 수정
|
||||||
first_delv_fee_element = await self.page.wait_for_selector(self.first_delv_fee_input_locator)
|
first_delv_fee_element = await self.page.wait_for_selector(self.first_delv_fee_input_locator, timeout=5000)
|
||||||
if first_delv_fee_element:
|
if first_delv_fee_element:
|
||||||
first_delv_fee = self.round_to_UP(first_delv_fee)
|
first_delv_fee = self.round_to_UP(first_delv_fee)
|
||||||
await first_delv_fee_element.fill(str(first_delv_fee)) # 기존 내용을 지우고 새 값을 입력
|
await first_delv_fee_element.fill(str(first_delv_fee)) # 기존 내용을 지우고 새 값을 입력
|
||||||
|
|
@ -167,7 +174,7 @@ class PriceHandler:
|
||||||
self.logger.error(f"초도배송비 입력 중 오류 발생: 요소를 찾을 수 없음", exc_info=True)
|
self.logger.error(f"초도배송비 입력 중 오류 발생: 요소를 찾을 수 없음", exc_info=True)
|
||||||
|
|
||||||
# 교환비 수정
|
# 교환비 수정
|
||||||
exchange_fee_element = await self.page.wait_for_selector(self.exchange_fee_input_locator)
|
exchange_fee_element = await self.page.wait_for_selector(self.exchange_fee_input_locator, timeout=5000)
|
||||||
if exchange_fee_element:
|
if exchange_fee_element:
|
||||||
exchange_fee = self.round_to_UP(exchange_fee)
|
exchange_fee = self.round_to_UP(exchange_fee)
|
||||||
await exchange_fee_element.fill(str(exchange_fee)) # 기존 내용을 지우고 새 값을 입력
|
await exchange_fee_element.fill(str(exchange_fee)) # 기존 내용을 지우고 새 값을 입력
|
||||||
|
|
@ -224,12 +231,12 @@ class PriceHandler:
|
||||||
shipping_cost = self.round_to_UP(shipping_cost)
|
shipping_cost = self.round_to_UP(shipping_cost)
|
||||||
|
|
||||||
# 더하기 마진 입력 (4번째 인풋박스)
|
# 더하기 마진 입력 (4번째 인풋박스)
|
||||||
margin_element = await self.page.wait_for_selector(self.plus_margin_locator)
|
margin_element = await self.page.wait_for_selector(self.plus_margin_locator, timeout=5000)
|
||||||
await margin_element.fill(str(margin))
|
await margin_element.fill(str(margin))
|
||||||
self.logger.debug(f"더하기 마진 입력 완료: {margin}")
|
self.logger.debug(f"더하기 마진 입력 완료: {margin}")
|
||||||
|
|
||||||
# 해외 배송비 입력 (5번째 인풋박스)
|
# 해외 배송비 입력 (5번째 인풋박스)
|
||||||
oversea_shipping_element = await self.page.wait_for_selector(self.oversea_shipping_locator)
|
oversea_shipping_element = await self.page.wait_for_selector(self.oversea_shipping_locator, timeout=5000)
|
||||||
await oversea_shipping_element.fill(str(shipping_cost))
|
await oversea_shipping_element.fill(str(shipping_cost))
|
||||||
self.logger.debug(f"해외 배송비 입력 완료: {shipping_cost}")
|
self.logger.debug(f"해외 배송비 입력 완료: {shipping_cost}")
|
||||||
|
|
||||||
|
|
@ -331,6 +338,10 @@ class PriceHandler:
|
||||||
- adjusted_margin (int): 적정 판매가에 맞게 조정된 더하기 마진.
|
- adjusted_margin (int): 적정 판매가에 맞게 조정된 더하기 마진.
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
|
if not option_data or len(option_data) == 0:
|
||||||
|
self.logger.warning(f"옵션 데이터가 없거나 비어있습니다. 계산원가 기준 더하기마진({self.initail_margin_cost}) 을 반환합니다.")
|
||||||
|
return self.initail_margin_cost # 기본 더하기 마진 설정
|
||||||
|
|
||||||
total_option_price = math.ceil(sum([option['price'] for option in option_data]) / len(option_data))
|
total_option_price = math.ceil(sum([option['price'] for option in option_data]) / len(option_data))
|
||||||
self.logger.debug(f"총 옵션 기준 판매가 평균: {total_option_price}")
|
self.logger.debug(f"총 옵션 기준 판매가 평균: {total_option_price}")
|
||||||
|
|
||||||
|
|
@ -503,11 +514,11 @@ class PriceHandler:
|
||||||
initail_shipping_cost = self.round_to_UP(initail_shipping_cost)
|
initail_shipping_cost = self.round_to_UP(initail_shipping_cost)
|
||||||
self.logger.debug(f"계산원가 기준 해외배송비: {initail_shipping_cost}")
|
self.logger.debug(f"계산원가 기준 해외배송비: {initail_shipping_cost}")
|
||||||
|
|
||||||
initail_margin_cost = self.calculate_shipping_cost_with_extended_thresholds(10000, initial_cost_price)
|
self.initail_margin_cost = self.calculate_shipping_cost_with_extended_thresholds(10000, initial_cost_price)
|
||||||
initail_margin_cost = self.round_to_UP(initail_margin_cost)
|
self.initail_margin_cost = self.round_to_UP(self.initail_margin_cost)
|
||||||
self.logger.debug(f"계산원가 기준 더하기마진: {initail_margin_cost}")
|
self.logger.debug(f"계산원가 기준 더하기마진: {self.initail_margin_cost}")
|
||||||
|
|
||||||
result = int(initial_cost_price + initail_shipping_cost + initail_margin_cost)
|
result = int(initial_cost_price + initail_shipping_cost + self.initail_margin_cost)
|
||||||
self.logger.debug(f"원가기반 가격: {result}")
|
self.logger.debug(f"원가기반 가격: {result}")
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
@ -528,7 +539,7 @@ class PriceHandler:
|
||||||
try:
|
try:
|
||||||
|
|
||||||
# 요소를 기다림
|
# 요소를 기다림
|
||||||
option_count_text_element = await self.page.wait_for_selector(self.option_count_text_locator)
|
option_count_text_element = await self.page.wait_for_selector(self.option_count_text_locator, timeout=5000)
|
||||||
|
|
||||||
# 텍스트에서 숫자만 추출
|
# 텍스트에서 숫자만 추출
|
||||||
option_text = await option_count_text_element.text_content()
|
option_text = await option_count_text_element.text_content()
|
||||||
|
|
@ -551,7 +562,7 @@ class PriceHandler:
|
||||||
return 1 # 예외 발생 시 기본값 1 반환
|
return 1 # 예외 발생 시 기본값 1 반환
|
||||||
|
|
||||||
|
|
||||||
async def collect_product_costs_and_prices(self):
|
async def collect_product_costs_and_prices(self, is_single):
|
||||||
"""
|
"""
|
||||||
상품 원가와 판매가를 수집하여 반환합니다. 단위는 위안화
|
상품 원가와 판매가를 수집하여 반환합니다. 단위는 위안화
|
||||||
|
|
||||||
|
|
@ -567,19 +578,29 @@ class PriceHandler:
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
# 옵션 개수 가져오기 (두 가지 방법 중 하나 사용)
|
# 옵션 개수 가져오기 (두 가지 방법 중 하나 사용)
|
||||||
|
if is_single:
|
||||||
|
total_options = 1
|
||||||
|
else:
|
||||||
total_options = await self.get_option_count_from_text()
|
total_options = await self.get_option_count_from_text()
|
||||||
|
|
||||||
self.product_costs = [] # 모든 옵션의 상품원가를 저장할 리스트
|
self.product_costs = [] # 모든 옵션의 상품원가를 저장할 리스트
|
||||||
self.option_data = [] # 각 옵션의 상품원가와 기준판매가를 저장할 리스트
|
self.option_data = [] # 각 옵션의 상품원가와 기준판매가를 저장할 리스트
|
||||||
|
|
||||||
|
self.logger.debug(f"collect_product_costs_and_prices 단일옵션 : {is_single}")
|
||||||
|
|
||||||
# 옵션 개수만큼 순회하면서 값을 수집
|
# 옵션 개수만큼 순회하면서 값을 수집
|
||||||
for i in range(1, total_options + 1):
|
for i in range(1, total_options + 1):
|
||||||
|
|
||||||
|
if is_single:
|
||||||
|
product_cost_locator = self.product_cost_for_single_locator
|
||||||
|
standard_selling_price_locator = self.standard_selling_price_for_single_locator
|
||||||
|
else:
|
||||||
product_cost_locator = self.product_cost_locator_template.format(index=i+1)
|
product_cost_locator = self.product_cost_locator_template.format(index=i+1)
|
||||||
standard_selling_price_locator = self.standard_selling_price_locator_template.format(index=i+1)
|
standard_selling_price_locator = self.standard_selling_price_locator_template.format(index=i+1)
|
||||||
|
|
||||||
# 각 선택자를 사용하여 요소 찾기
|
# 각 선택자를 사용하여 요소 찾기
|
||||||
product_cost_element = await self.page.wait_for_selector(product_cost_locator)
|
product_cost_element = await self.page.wait_for_selector(product_cost_locator, timeout=5000)
|
||||||
standard_selling_price_element = await self.page.wait_for_selector(standard_selling_price_locator)
|
standard_selling_price_element = await self.page.wait_for_selector(standard_selling_price_locator, timeout=5000)
|
||||||
|
|
||||||
# cost_value = int(float(await product_cost_element.input_value().replace(",", "")))
|
# cost_value = int(float(await product_cost_element.input_value().replace(",", "")))
|
||||||
cost_value_str = await product_cost_element.input_value()
|
cost_value_str = await product_cost_element.input_value()
|
||||||
|
|
@ -634,12 +655,12 @@ class PriceHandler:
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
# 더하기 마진 값 가져오기
|
# 더하기 마진 값 가져오기
|
||||||
margin_element = await self.page.wait_for_selector(self.plus_margin_locator)
|
margin_element = await self.page.wait_for_selector(self.plus_margin_locator, timeout=5000)
|
||||||
margin_value = await margin_element.input_value()
|
margin_value = await margin_element.input_value()
|
||||||
margin = int(margin_value.replace(",", "")) if margin_value else 0
|
margin = int(margin_value.replace(",", "")) if margin_value else 0
|
||||||
self.logger.debug(f"더하기 마진 값: {margin}")
|
self.logger.debug(f"더하기 마진 값: {margin}")
|
||||||
|
|
||||||
shipping_element = await self.page.wait_for_selector(self.oversea_shipping_locator)
|
shipping_element = await self.page.wait_for_selector(self.oversea_shipping_locator, timeout=5000)
|
||||||
shipping_value = await shipping_element.input_value()
|
shipping_value = await shipping_element.input_value()
|
||||||
shipping_cost = int(shipping_value.replace(",", "")) if shipping_value else 0
|
shipping_cost = int(shipping_value.replace(",", "")) if shipping_value else 0
|
||||||
self.logger.debug(f"해외 배송비 값: {shipping_cost}")
|
self.logger.debug(f"해외 배송비 값: {shipping_cost}")
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
{
|
{
|
||||||
"option_prompt_template": "질문은 아래와 같아.\n\n가공방법\n1. 특수문자가 있을 경우 제거해줘.\n2. 원본 상품명 '{product_name}'을 참고해서, 각 옵션의 이름을 최대한 간결하게, 각 옵션의 특징(제품의 스펙을 나타내는 크기,무게,용량,전압,전류,상품코드등)만 남겨줘.\n3. 간결하게 만들어진 각 옵션명을 한국어로 일관되게 번역해줘.\n4. 번역된 옵션 이름 중 같은 이름의 옵션이 있을 경우, 해당옵션들만 원본옵션명에서 특징들을 다시 추출해서 추가해줘.\n5. 가격문의, 문의사항, 견적, 견적문의, 예약금, 선결제, 고객센터 연락 등 옵션명을 바로 알려주지 않고 고객에게 연락을 유도하는 옵션명은 삭제해야 해.\n6. 짧은 단어로 대체할수 있는 단어들은 (예시 : 디스플레시 > 화면)의미를 유지한체 짧은 단어로 대체해줘.\n7. 번역된 옵션명들은 'trans_option_1': '', 'trans_option_2': '', 'trans_option_3': '', 'trans_option_4': '' 와 같은 형식(json)으로 반환해줘.\n\n원본 데이터\n 원본옵션명{options}",
|
"option_prompt_template": "질문은 아래와 같아.\n\n가공방법\n1. 특수문자가 있을 경우 제거해줘.\n2. 원본 상품명 '{product_name}'을 참고해서, 각 옵션의 이름을 최대한 간결하게, 각 옵션의 특징(제품의 스펙을 나타내는 크기,무게,용량,전압,전류,상품코드등)만 남겨줘.\n3. 간결하게 만들어진 각 옵션명을 한국어로 일관되게 번역해줘. 중국어가 남아 있어선 안되!\n4. 번역된 옵션 이름 중 같은 이름의 옵션이 있을 경우, 해당옵션들만 원본옵션명에서 특징들을 다시 추출해서 추가해줘. 중국어가 남아 있어선 안되!\n5. 가격문의, 문의사항, 견적, 견적문의, 예약금, 선결제, 고객센터 연락 등 옵션명을 바로 알려주지 않고 고객에게 연락을 유도하는 옵션명은 삭제해야 해.\n6. 짧은 단어로 대체할수 있는 단어들은 (예시 : 디스플레시 > 화면)의미를 유지한체 짧은 단어로 대체해줘.\n7. 번역된 옵션명들은 'trans_option_1': '', 'trans_option_2': '', 'trans_option_3': '', 'trans_option_4': '' 와 같은 형식(json)으로 반환해줘.\n\n원본 데이터\n 원본옵션명{options}",
|
||||||
"option_prompt_template_old": "질문은 아래와 같아.\n\n가공방법\n1. 특수문자가 있을 경우 제거해줘.\n2. 원본 상품명을 참고해서, 각 옵션의 이름을 최대한 간결하게, 각 옵션의 특징(제품의 스펙을 나타내는 크기,무게,용량,전압,전류,상품코드등)만 남겨줘.\n3. 간결하게 만들어진 각 옵션명을 한국어로 일관되게 번역해줘.\n4. 번역된 옵션 이름 중 같은 이름의 옵션이 있을 경우, 해당옵션들만 원본옵션명에서 특징들을 다시 추출해서 추가해줘.\n5. 가격문의, 문의사항, 견적, 견적문의, 예약금, 선결제, 고객센터 연락 등 옵션명을 바로 알려주지 않고 고객에게 연락을 유도하는 옵션명은 삭제해야 해.\n6. 짧은 단어로 대체할수 있는 단어들은 (예시 : 디스플레시 > 화면)의미를 유지한체 짧은 단어로 대체해줘.\n7. 번역된 옵션명들은 'trans_option_1': '', 'trans_option_2': '', 'trans_option_3': '', 'trans_option_4': '' 와 같은 형식(json)으로 반환해줘.\n\n원본 데이터\n원본상품명'{product_name}'\n 원본옵션명{options}",
|
"option_prompt_template_old": "질문은 아래와 같아.\n\n가공방법\n1. 특수문자가 있을 경우 제거해줘.\n2. 원본 상품명을 참고해서, 각 옵션의 이름을 최대한 간결하게, 각 옵션의 특징(제품의 스펙을 나타내는 크기,무게,용량,전압,전류,상품코드등)만 남겨줘.\n3. 간결하게 만들어진 각 옵션명을 한국어로 일관되게 번역해줘.\n4. 번역된 옵션 이름 중 같은 이름의 옵션이 있을 경우, 해당옵션들만 원본옵션명에서 특징들을 다시 추출해서 추가해줘.\n5. 가격문의, 문의사항, 견적, 견적문의, 예약금, 선결제, 고객센터 연락 등 옵션명을 바로 알려주지 않고 고객에게 연락을 유도하는 옵션명은 삭제해야 해.\n6. 짧은 단어로 대체할수 있는 단어들은 (예시 : 디스플레시 > 화면)의미를 유지한체 짧은 단어로 대체해줘.\n7. 번역된 옵션명들은 'trans_option_1': '', 'trans_option_2': '', 'trans_option_3': '', 'trans_option_4': '' 와 같은 형식(json)으로 반환해줘.\n\n원본 데이터\n원본상품명'{product_name}'\n 원본옵션명{options}",
|
||||||
"detail_page_prompt_template": "상세 페이지 번역 요청: {detail_page}",
|
"detail_page_prompt_template": "상세 페이지 번역 요청: {detail_page}",
|
||||||
"title_prompt_template": "제목 번역 요청: {title}",
|
"title_prompt_template": "제목 번역 요청: {title}",
|
||||||
|
|
|
||||||
|
|
@ -765,16 +765,17 @@ class CMBSettingsDialog(QDialog):
|
||||||
query = '''
|
query = '''
|
||||||
SELECT id, category1, category2, category3, category4, crmobi_stage
|
SELECT id, category1, category2, category3, category4, crmobi_stage
|
||||||
FROM categories
|
FROM categories
|
||||||
WHERE category1 LIKE ? OR category2 LIKE ? OR category3 LIKE ? OR category4 LIKE ?
|
WHERE category1 LIKE :search_text OR category2 LIKE :search_text
|
||||||
|
OR category3 LIKE :search_text OR category4 LIKE :search_text
|
||||||
'''
|
'''
|
||||||
args = [f"%{search_text}%"] * 4
|
args = {"search_text": f"%{search_text}%"} # 사전 형태로 변경
|
||||||
else:
|
else:
|
||||||
# 검색어가 없는 경우 전체 로드
|
# 검색어가 없는 경우 전체 로드
|
||||||
query = '''
|
query = '''
|
||||||
SELECT id, category1, category2, category3, category4, crmobi_stage
|
SELECT id, category1, category2, category3, category4, crmobi_stage
|
||||||
FROM categories
|
FROM categories
|
||||||
'''
|
'''
|
||||||
args = []
|
args = {}
|
||||||
|
|
||||||
# 검색 결과를 트리에 표시
|
# 검색 결과를 트리에 표시
|
||||||
self.category_tree.clear()
|
self.category_tree.clear()
|
||||||
|
|
@ -812,53 +813,53 @@ class CMBSettingsDialog(QDialog):
|
||||||
self.logger.error(f"카테고리 검색 중 오류: {e}", exc_info=True)
|
self.logger.error(f"카테고리 검색 중 오류: {e}", exc_info=True)
|
||||||
|
|
||||||
|
|
||||||
def get_crmobi_stage_by_keyword(self, category):
|
# def get_crmobi_stage_by_keyword(self, category):
|
||||||
"""
|
# """
|
||||||
주어진 카테고리에 해당하는 CMB 단계가 설정되어 있는지 확인하고,
|
# 주어진 카테고리에 해당하는 CMB 단계가 설정되어 있는지 확인하고,
|
||||||
설정된 경우 해당 단계의 범위를 반환합니다.
|
# 설정된 경우 해당 단계의 범위를 반환합니다.
|
||||||
|
|
||||||
Parameters:
|
# Parameters:
|
||||||
category (str): 카테고리 이름 (예: '가구/인테리어')
|
# category (str): 카테고리 이름 (예: '가구/인테리어')
|
||||||
|
|
||||||
Returns:
|
# Returns:
|
||||||
tuple: (min_amount, unit_amount, extra_cost) 값이 있는 경우
|
# tuple: (min_amount, unit_amount, extra_cost) 값이 있는 경우
|
||||||
None: 설정된 CMB 단계가 없는 경우
|
# None: 설정된 CMB 단계가 없는 경우
|
||||||
"""
|
# """
|
||||||
try:
|
# try:
|
||||||
# DB에서 카테고리에 대한 CMB 단계 정보를 가져옴
|
# # DB에서 카테고리에 대한 CMB 단계 정보를 가져옴
|
||||||
query = '''
|
# query = '''
|
||||||
SELECT crmobi_stage FROM categories
|
# SELECT crmobi_stage FROM categories
|
||||||
WHERE category1 = ? OR category2 = ? OR category3 = ? OR category4 = ?
|
# WHERE category1 = ? OR category2 = ? OR category3 = ? OR category4 = ?
|
||||||
'''
|
# '''
|
||||||
|
|
||||||
self.logger.debug(f"category : {category}")
|
# self.logger.debug(f"category : {category}")
|
||||||
args = [category] * 4
|
# args = [category] * 4
|
||||||
# self.db_manager.execute(query, args)
|
# # self.db_manager.execute(query, args)
|
||||||
result = self.db_manager.fetchone(query, args)
|
# result = self.db_manager.fetchone(query, args)
|
||||||
|
|
||||||
if result and result[0] > 0: # CMB 단계가 설정되어 있을 때
|
# if result and result[0] > 0: # CMB 단계가 설정되어 있을 때
|
||||||
crmobi_stage = result[0]
|
# crmobi_stage = result[0]
|
||||||
|
|
||||||
# 설정된 CMB 단계에 해당하는 범위 가져오기
|
# # 설정된 CMB 단계에 해당하는 범위 가져오기
|
||||||
stage_query = '''
|
# stage_query = '''
|
||||||
SELECT threshold, increment_unit, extra_cost
|
# SELECT threshold, increment_unit, extra_cost
|
||||||
FROM crmobi_stages
|
# FROM crmobi_stages
|
||||||
WHERE stage = ?
|
# WHERE stage = ?
|
||||||
'''
|
# '''
|
||||||
# self.db_manager.execute(stage_query, (crmobi_stage,))
|
# # self.db_manager.execute(stage_query, (crmobi_stage,))
|
||||||
stage_result = self.db_manager.fetchone(stage_query, (crmobi_stage,))
|
# stage_result = self.db_manager.fetchone(stage_query, (crmobi_stage,))
|
||||||
self.logger.debug(f"stage_result : {stage_result}")
|
# self.logger.debug(f"stage_result : {stage_result}")
|
||||||
|
|
||||||
if stage_result:
|
# if stage_result:
|
||||||
min_amount, unit_amount, extra_cost = stage_result
|
# min_amount, unit_amount, extra_cost = stage_result
|
||||||
return (min_amount, unit_amount, extra_cost)
|
# return (min_amount, unit_amount, extra_cost)
|
||||||
|
|
||||||
# CMB 단계가 설정되지 않은 경우
|
# # CMB 단계가 설정되지 않은 경우
|
||||||
return None
|
# return None
|
||||||
|
|
||||||
except Exception as e:
|
# except Exception as e:
|
||||||
QMessageBox.critical(self, "DB 오류", f"DB 조회 중 오류가 발생했습니다: {e}")
|
# QMessageBox.critical(self, "DB 오류", f"DB 조회 중 오류가 발생했습니다: {e}")
|
||||||
return None
|
# return None
|
||||||
|
|
||||||
|
|
||||||
def get_crmobi_stage(self, category):
|
def get_crmobi_stage(self, category):
|
||||||
|
|
|
||||||
|
After Width: | Height: | Size: 1.5 KiB |
|
After Width: | Height: | Size: 1.8 KiB |
|
After Width: | Height: | Size: 464 B |
|
After Width: | Height: | Size: 1.8 KiB |
|
After Width: | Height: | Size: 1.5 KiB |
|
After Width: | Height: | Size: 1.8 KiB |
|
|
@ -0,0 +1,297 @@
|
||||||
|
import pyautogui
|
||||||
|
import time
|
||||||
|
import os
|
||||||
|
import subprocess
|
||||||
|
import win32gui, win32con, win32process
|
||||||
|
import pyscreeze
|
||||||
|
|
||||||
|
class WhaleTranslator:
|
||||||
|
def __init__(self, logger, error_image_filenames=['fail_translated1.png', 'fail_translated2.png'], pixel_check_interval=0.1, secret_mode=True, timeout=20, color_tolerance=20):
|
||||||
|
self.logger = logger
|
||||||
|
self.error_image_paths = [os.path.join(os.path.dirname(__file__), filename) for filename in error_image_filenames]
|
||||||
|
|
||||||
|
self.page_loading_icon_path = os.path.join(os.path.dirname(__file__), 'page_loading.png')
|
||||||
|
self.translating_image_path = os.path.join(os.path.dirname(__file__), 'translating.png')
|
||||||
|
# self.translating_image_path = os.path.join(os.path.dirname(__file__), 'src', 'img', 'translating.png')
|
||||||
|
|
||||||
|
|
||||||
|
self.pixel_check_interval = pixel_check_interval
|
||||||
|
self.whale_window_name = "새 시크릿 탭 - Whale" if secret_mode else "새 탭 - Whale"
|
||||||
|
self.whale_pid = None
|
||||||
|
self.timeout = timeout # 번역 성공 여부를 판단하기 위한 시간 제한 설정
|
||||||
|
self.color_tolerance = color_tolerance # 색상 허용 오차
|
||||||
|
self.colors = {'before': None, 'during': None, 'after': None} # 색상 기록
|
||||||
|
self.whale_rect = None
|
||||||
|
|
||||||
|
def start_whale_browser(self, url):
|
||||||
|
whale_path = r"C:\\Program Files\\Naver\\Naver Whale\\Application\\whale.exe"
|
||||||
|
process = subprocess.Popen([whale_path, '--incognito'])
|
||||||
|
time.sleep(2)
|
||||||
|
self.whale_pid = process.pid
|
||||||
|
self.logger.debug(f"Whale 브라우저 실행, PID: {self.whale_pid}")
|
||||||
|
|
||||||
|
hwnd = self.find_whale_window()
|
||||||
|
if hwnd:
|
||||||
|
self.set_window_position(hwnd, 1, 1, 1280, 720) # 위치 (100, 100), 크기 (1280x720)
|
||||||
|
self.update_whale_rect()
|
||||||
|
|
||||||
|
if hwnd:
|
||||||
|
win32gui.SetForegroundWindow(hwnd)
|
||||||
|
pyautogui.hotkey('ctrl', 'l')
|
||||||
|
time.sleep(0.5)
|
||||||
|
pyautogui.typewrite(url)
|
||||||
|
pyautogui.press('enter')
|
||||||
|
time.sleep(2)
|
||||||
|
|
||||||
|
size = self.get_whale_window_title()
|
||||||
|
|
||||||
|
# # 페이지 로딩 완료 대기
|
||||||
|
# self.wait_for_loading_icon_to_disappear()
|
||||||
|
|
||||||
|
self.move_mouse_to_center() # 마우스 중앙으로 이동
|
||||||
|
time.sleep(0.5) # 마우스 이동 후 대기
|
||||||
|
|
||||||
|
original_color = self.get_mouse_position_color() # 현재 색상 가져오기
|
||||||
|
self.colors['before'] = original_color
|
||||||
|
self.logger.debug(f"번역 전 색상: {original_color}")
|
||||||
|
|
||||||
|
# 우클릭 및 번역 시작
|
||||||
|
pyautogui.rightClick()
|
||||||
|
time.sleep(1)
|
||||||
|
pyautogui.press('r')
|
||||||
|
|
||||||
|
# # 번역 시작 감지 (translating.png 확인)
|
||||||
|
# if self.is_translating():
|
||||||
|
# self.logger.debug("번역 시작 감지 ('translating.png' 발견)")
|
||||||
|
|
||||||
|
# 번역 중 색상 지속 확인
|
||||||
|
result = self.check_translation_by_color_change()
|
||||||
|
|
||||||
|
if result == "success":
|
||||||
|
self.logger.debug(f"URL: {url} - 번역이 성공적으로 완료되었습니다!")
|
||||||
|
elif result == "error":
|
||||||
|
self.logger.debug(f"URL: {url} - 번역에 실패했습니다.")
|
||||||
|
else:
|
||||||
|
self.logger.debug(f"URL: {url} - 번역 상태를 확인하지 못했습니다.")
|
||||||
|
|
||||||
|
# 최종 색상 출력
|
||||||
|
self.logger.debug(f"URL: {url} - 번역 중 색상: {self.colors['during']}")
|
||||||
|
self.logger.debug(f"URL: {url} - 번역 후 색상: {self.colors['after']}")
|
||||||
|
|
||||||
|
def wait_for_loading_icon_to_disappear(self, max_wait=20):
|
||||||
|
"""
|
||||||
|
로딩 아이콘이 화면에서 사라질 때까지 대기합니다.
|
||||||
|
max_wait: 최대 대기 시간 (초)
|
||||||
|
"""
|
||||||
|
start_time = time.time()
|
||||||
|
while time.time() - start_time < max_wait:
|
||||||
|
try:
|
||||||
|
# 화면에서 아이콘 위치 확인 시도
|
||||||
|
icon_location = self.find_image_with_confidence(self.page_loading_icon_path, confidence=0.9)
|
||||||
|
if icon_location:
|
||||||
|
self.logger.debug("페이지 로딩 중...")
|
||||||
|
time.sleep(0.5) # 간격을 두고 다시 확인
|
||||||
|
|
||||||
|
except pyautogui.ImageNotFoundException as e:
|
||||||
|
# 아이콘이 화면에 없으면 로딩 완료로 간주
|
||||||
|
# self.logger.debug(f"페이지 로딩이 완료되었습니다. {e}", exc_info=True)
|
||||||
|
self.logger.debug(f"페이지 로딩이 완료되었습니다.")
|
||||||
|
return True # 로딩 완료
|
||||||
|
|
||||||
|
self.logger.debug("로딩 완료 대기 시간이 초과되었습니다.")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def find_image_with_confidence(self, image_path, confidence=0.8):
|
||||||
|
"""이미지를 찾을 때 highest confidence 값을 로그로 출력하는 메서드"""
|
||||||
|
if self.whale_rect:
|
||||||
|
|
||||||
|
# 웨일 창의 크기를 region으로 설정
|
||||||
|
region = (self.whale_rect[0], self.whale_rect[1],
|
||||||
|
self.whale_rect[2] - self.whale_rect[0],
|
||||||
|
self.whale_rect[3] - self.whale_rect[1])
|
||||||
|
self.logger.debug(f"이미지를 찾을 영역: {region}")
|
||||||
|
|
||||||
|
try:
|
||||||
|
# locateOnScreen 시도
|
||||||
|
result = pyscreeze.locateOnScreen(image_path, confidence=confidence, region=region)
|
||||||
|
|
||||||
|
if result:
|
||||||
|
self.logger.debug(f"이미지를 찾았습니다: {image_path}")
|
||||||
|
return result
|
||||||
|
else:
|
||||||
|
raise pyscreeze.ImageNotFoundException("Could not locate the image")
|
||||||
|
|
||||||
|
except pyscreeze.ImageNotFoundException as e:
|
||||||
|
# 'Could not locate the image' 메시지 발생 시 최고 confidence 값 로그 출력
|
||||||
|
if hasattr(e, 'args') and 'highest confidence' in str(e.args[0]):
|
||||||
|
self.logger.error(f"{image_path} 이미지를 찾지 못했습니다만 유사한 부분을 찾았습니다. : [{e.args[0]}]")
|
||||||
|
else:
|
||||||
|
self.logger.error(f"{image_path}이미지를 찾지 못했습니다. 이미지와 유사한 부분을 찾지 못했습니다.")
|
||||||
|
return None
|
||||||
|
|
||||||
|
def is_translating(self):
|
||||||
|
"""translating.png가 화면에 나타나는지 확인하여 번역 시작 여부를 판단"""
|
||||||
|
try:
|
||||||
|
# translating_location = pyautogui.locateOnScreen(self.translating_image_path, confidence=0.8)
|
||||||
|
translating_location = self.find_image_with_confidence(self.translating_image_path, confidence=0.4)
|
||||||
|
if translating_location:
|
||||||
|
return True
|
||||||
|
except pyautogui.ImageNotFoundException as e:
|
||||||
|
self.logger.debug(f"'translating.png'를 찾지 못했습니다. : {e}", exc_info=True)
|
||||||
|
return False
|
||||||
|
|
||||||
|
def update_whale_rect(self):
|
||||||
|
"""웨일 창의 위치 및 크기 rect를 업데이트"""
|
||||||
|
if self.whale_hwnd:
|
||||||
|
self.whale_rect = win32gui.GetWindowRect(self.whale_hwnd)
|
||||||
|
self.logger.debug(f"웨일 창 크기 및 위치 저장: {self.whale_rect}")
|
||||||
|
|
||||||
|
def get_whale_window_title(self):
|
||||||
|
"""
|
||||||
|
현재 활성화된 웨일 창의 이름을 가져옵니다.
|
||||||
|
제목에서 이미지의 해상도를 확인한 후, 일정 크기 이하인 경우 작업을 패스하도록 처리합니다.
|
||||||
|
"""
|
||||||
|
# hwnd = self.find_whale_window() # 웨일 창 핸들 가져오기
|
||||||
|
if self.whale_hwnd:
|
||||||
|
window_title = win32gui.GetWindowText(self.whale_hwnd)
|
||||||
|
self.logger.debug(f"현재 웨일 창의 제목: {window_title}")
|
||||||
|
|
||||||
|
# 해상도를 추출하기 위해 제목에서 괄호 안의 (숫자×숫자) 부분을 찾음
|
||||||
|
import re
|
||||||
|
match = re.search(r"\((\d+)×(\d+)\)", window_title)
|
||||||
|
if match:
|
||||||
|
width = int(match.group(1))
|
||||||
|
height = int(match.group(2))
|
||||||
|
self.logger.debug(f"이미지 해상도: {width}×{height}")
|
||||||
|
|
||||||
|
# 해상도가 기준보다 작은 경우 패스
|
||||||
|
if width < 300 or height < 200:
|
||||||
|
self.logger.debug(f"이미지 해상도가 너무 작습니다. ({width}×{height}), 작업을 패스합니다.")
|
||||||
|
return False # 작업을 수행하지 않음
|
||||||
|
return True # 작업을 계속 진행
|
||||||
|
|
||||||
|
else:
|
||||||
|
self.logger.error("이미지 해상도를 가져오지 못했습니다.")
|
||||||
|
return False
|
||||||
|
|
||||||
|
self.logger.error("웨일 창을 찾을 수 없습니다.")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def find_whale_window(self):
|
||||||
|
"""웨일 창을 제목을 기준으로 찾는 메서드"""
|
||||||
|
def callback(hwnd, extra):
|
||||||
|
if win32gui.IsWindowVisible(hwnd):
|
||||||
|
title = win32gui.GetWindowText(hwnd)
|
||||||
|
if self.whale_window_name in title:
|
||||||
|
extra.append(hwnd)
|
||||||
|
|
||||||
|
hwnd_list = []
|
||||||
|
win32gui.EnumWindows(callback, hwnd_list)
|
||||||
|
if hwnd_list:
|
||||||
|
self.whale_hwnd = hwnd_list[0]
|
||||||
|
self.logger.debug(f"웨일 창을 찾았습니다: {self.whale_hwnd}")
|
||||||
|
self.update_whale_rect()
|
||||||
|
return self.whale_hwnd
|
||||||
|
else:
|
||||||
|
self.logger.debug("웨일 창을 찾지 못했습니다.")
|
||||||
|
return None
|
||||||
|
|
||||||
|
def set_window_position(self, hwnd, x, y, width, height):
|
||||||
|
"""지정된 위치와 크기로 창을 조정"""
|
||||||
|
win32gui.ShowWindow(hwnd, win32con.SW_RESTORE)
|
||||||
|
win32gui.SetWindowPos(hwnd, None, x, y, width, height, win32con.SWP_NOZORDER | win32con.SWP_NOACTIVATE)
|
||||||
|
self.logger.debug(f"창 위치 및 크기 설정: 위치({x}, {y}), 크기({width}x{height})")
|
||||||
|
|
||||||
|
def move_mouse_to_center(self):
|
||||||
|
"""웨일 브라우저 창의 중앙으로 마우스 커서를 이동"""
|
||||||
|
if self.whale_rect:
|
||||||
|
center_x = (self.whale_rect[0] + self.whale_rect[2]) // 2 # 가로 중앙 계산
|
||||||
|
center_y = (self.whale_rect[1] + self.whale_rect[3]) // 2 # 세로 중앙 계산
|
||||||
|
|
||||||
|
center_y = center_y + 50
|
||||||
|
pyautogui.moveTo(center_x, center_y)
|
||||||
|
self.logger.debug(f"마우스 커서를 창 중앙으로 이동: ({center_x}, {center_y})")
|
||||||
|
else:
|
||||||
|
self.logger.error("웨일 창의 크기를 알 수 없습니다. 먼저 창을 찾으세요.")
|
||||||
|
|
||||||
|
def is_color_changed(self, current_color):
|
||||||
|
"""현재 색상이 변화했는지 확인 (필터 제거 여부)"""
|
||||||
|
if self.colors['during'] is None:
|
||||||
|
self.colors['during'] = current_color
|
||||||
|
return not self.is_similar_color(current_color, self.colors['during'])
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def check_translation_by_color_change(self):
|
||||||
|
start_time = time.time()
|
||||||
|
|
||||||
|
# 번역 시작 감지 (translating.png 확인)
|
||||||
|
while time.time() - start_time < self.timeout:
|
||||||
|
current_color = self.get_mouse_position_color()
|
||||||
|
self.logger.debug(f"현재 색상: {current_color}")
|
||||||
|
|
||||||
|
# 필터가 사라져서 색상이 변했는지 확인
|
||||||
|
if self.is_color_changed(current_color):
|
||||||
|
self.colors['after'] = current_color
|
||||||
|
self.logger.debug("색상 변화 감지 (필터 제거됨)")
|
||||||
|
|
||||||
|
# translating.png가 여전히 존재하는지 확인하여 번역 성공 여부 판단
|
||||||
|
result = self.find_image_with_confidence(self.translating_image_path, confidence=0.4)
|
||||||
|
if result:
|
||||||
|
return "success"
|
||||||
|
else:
|
||||||
|
return "error"
|
||||||
|
else:
|
||||||
|
# 번역 실패 이미지 확인
|
||||||
|
if self.is_translation_failed():
|
||||||
|
self.colors['after'] = current_color
|
||||||
|
return "error"
|
||||||
|
|
||||||
|
time.sleep(self.pixel_check_interval)
|
||||||
|
|
||||||
|
# 타임아웃 발생 시 translating.png 여부로 번역 성공/실패 판단
|
||||||
|
self.colors['after'] = current_color
|
||||||
|
result = self.find_image_with_confidence(self.translating_image_path, confidence=0.4)
|
||||||
|
if result:
|
||||||
|
self.logger.debug("번역 성공으로 간주 (타임아웃 후 translating.png 존재)")
|
||||||
|
return "success"
|
||||||
|
else:
|
||||||
|
self.logger.debug("번역 실패로 간주 (타임아웃 후 translating.png 없음)")
|
||||||
|
return "error"
|
||||||
|
|
||||||
|
def is_similar_color(self, color1, color2):
|
||||||
|
"""색상이 유사한지 확인 (허용 오차 적용)"""
|
||||||
|
r_diff = abs(color1[0] - color2[0])
|
||||||
|
g_diff = abs(color1[1] - color2[1])
|
||||||
|
b_diff = abs(color1[2] - color2[2])
|
||||||
|
return r_diff < self.color_tolerance and g_diff < self.color_tolerance and b_diff < self.color_tolerance
|
||||||
|
|
||||||
|
def get_mouse_position_color(self):
|
||||||
|
x, y = pyautogui.position()
|
||||||
|
return pyautogui.pixel(x, y)
|
||||||
|
|
||||||
|
def is_translation_failed(self):
|
||||||
|
"""번역 실패 이미지 확인"""
|
||||||
|
for image_path in self.error_image_paths:
|
||||||
|
result = self.find_image_with_confidence(image_path, confidence=0.8)
|
||||||
|
if result:
|
||||||
|
self.logger.error(f"번역 실패: '{os.path.basename(image_path)}' 메시지가 감지되었습니다.")
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
# 예제 사용법
|
||||||
|
import logging
|
||||||
|
|
||||||
|
# 로거 설정
|
||||||
|
logging.basicConfig(level=logging.DEBUG)
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
# 클래스 인스턴스 생성 및 브라우저 실행 후 번역 상태 확인
|
||||||
|
translator = WhaleTranslator(logger, ['fail_translated1.png', 'fail_translated2.png'])
|
||||||
|
|
||||||
|
# 브라우저 실행 후 URL로 이동 및 번역 성공 여부 확인
|
||||||
|
translator.start_whale_browser('https://img.alicdn.com/imgextra/i3/350475995/O1CN01uFwQ9v1u9kwOuU78C-350475995.png_Q75.jpg')
|
||||||
|
translator.start_whale_browser("https://file.percenty.co.kr/public/652bed8e865b1f32ea62bf1f/products/66ff967773994c46d388bb36/82d07178-ae60-49f7-a489-e02801ff7b06.jpg")
|
||||||
|
translator.start_whale_browser('https://img.alicdn.com/imgextra/i4/735691568/O1CN01sRUYqb1NSBuefMBlw_!!735691568.jpg_Q75.jpg')
|
||||||
|
translator.start_whale_browser('https://img.alicdn.com/imgextra/i4/1773313923/O1CN01VMRs1Z1eqmfYSXQDu_!!1773313923.jpg_Q75.jpg')
|
||||||
|
|
||||||
|
After Width: | Height: | Size: 464 B |
|
After Width: | Height: | Size: 327 B |
|
After Width: | Height: | Size: 4.0 KiB |
7
title.py
|
|
@ -210,6 +210,13 @@ class TitleHandler:
|
||||||
if await category_text_certified_element.count():
|
if await category_text_certified_element.count():
|
||||||
category_text = await category_text_certified_element.inner_text()
|
category_text = await category_text_certified_element.inner_text()
|
||||||
self.logger.debug(f"인증 필요 카테고리 text = {category_text}")
|
self.logger.debug(f"인증 필요 카테고리 text = {category_text}")
|
||||||
|
if "그룹상품" in category_text:
|
||||||
|
self.logger.debug(f"카테고리 그룹상품 발생 category_text = {category_text}")
|
||||||
|
category_text_certified_element = main_category_element.locator(self.category_text_locator_certified)
|
||||||
|
|
||||||
|
if await category_text_certified_element.count():
|
||||||
|
category_text = await category_text_certified_element.inner_text()
|
||||||
|
self.logger.debug(f"그룹상품 카테고리 text = {category_text}")
|
||||||
else:
|
else:
|
||||||
self.logger.debug(f"카테고리 text = {category_text}")
|
self.logger.debug(f"카테고리 text = {category_text}")
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,26 +1,41 @@
|
||||||
import pyautogui
|
import pyautogui
|
||||||
import ctypes
|
import os
|
||||||
import time
|
import time
|
||||||
import win32gui, win32con, win32process
|
import win32gui, win32con, win32process
|
||||||
from pyvda import VirtualDesktop, get_virtual_desktops
|
from pyvda import VirtualDesktop, get_virtual_desktops
|
||||||
import subprocess
|
import subprocess
|
||||||
import asyncio
|
import pyscreeze
|
||||||
import KO_EN
|
import KO_EN
|
||||||
import pyperclip # 클립보드 데이터를 확인하기 위한 라이브러리
|
import pyperclip # 클립보드 데이터를 확인하기 위한 라이브러리
|
||||||
from PIL import ImageGrab
|
from PIL import ImageGrab
|
||||||
|
import re
|
||||||
|
|
||||||
class WhaleTranslator:
|
class WhaleTranslator:
|
||||||
def __init__(self, app, logger, secret_mode=True, vd_mode=False, max_failures=5):
|
def __init__(self, app, logger, secret_mode=True, vd_mode=False, pixel_check_interval=0.1, timeout=10, color_tolerance=20):
|
||||||
self.app = app
|
self.app = app
|
||||||
self.logger = logger
|
self.logger = logger
|
||||||
self.vd_mode = vd_mode
|
self.vd_mode = vd_mode
|
||||||
self.newtab = "about:newtab"
|
self.newtab = "about:newtab"
|
||||||
self.whale_pid = None
|
self.whale_pid = None
|
||||||
|
self.whale_rect = None
|
||||||
isSecret = secret_mode
|
isSecret = secret_mode
|
||||||
|
|
||||||
|
self.pixel_check_interval = pixel_check_interval
|
||||||
|
self.timeout = timeout # 번역 성공 여부를 판단하기 위한 시간 제한 설정
|
||||||
|
self.color_tolerance = color_tolerance # 색상 허용 오차
|
||||||
|
self.colors = {'before': None, 'during': None, 'after': None} # 색상 기록
|
||||||
|
|
||||||
|
# main.py 실행 경로의 하위 폴더(src/img)에서 이미지 파일 경로 설정
|
||||||
|
base_dir = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
img_dir = os.path.join(base_dir, 'src', 'img')
|
||||||
|
|
||||||
|
error_image_filenames = ['fail_translated1.png', 'fail_translated2.png']
|
||||||
|
self.error_image_paths = [os.path.join(img_dir, filename) for filename in error_image_filenames]
|
||||||
|
self.page_loading_icon_path = os.path.join(img_dir, 'page_loading.png')
|
||||||
|
self.translating_image_path = os.path.join(img_dir, 'translating.png')
|
||||||
|
|
||||||
self.translation_success_flag = False # 번역 성공 플래그
|
self.translation_success_flag = False # 번역 성공 플래그
|
||||||
self.failure_count = 0 # 실패 횟수
|
self.failure_count = 0 # 실패 횟수
|
||||||
self.max_failures = max_failures # 최대 실패 횟수
|
|
||||||
|
|
||||||
if isSecret:
|
if isSecret:
|
||||||
self.whale_window_name = "새 시크릿 탭 - Whale"
|
self.whale_window_name = "새 시크릿 탭 - Whale"
|
||||||
|
|
@ -59,9 +74,12 @@ class WhaleTranslator:
|
||||||
return
|
return
|
||||||
|
|
||||||
# 창 크기 조절 및 포커스 이동
|
# 창 크기 조절 및 포커스 이동
|
||||||
win32gui.ShowWindow(self.whale_hwnd, win32con.SW_NORMAL)
|
# win32gui.ShowWindow(self.whale_hwnd, win32con.SW_NORMAL)
|
||||||
win32gui.SetWindowPos(self.whale_hwnd, None, 0, 0, 1920, 1080, win32con.SWP_NOZORDER)
|
# win32gui.SetWindowPos(self.whale_hwnd, None, 0, 0, 1920, 1080, win32con.SWP_NOZORDER)
|
||||||
self.logger.debug("Whale 창 크기 조절 완료")
|
# self.logger.debug("Whale 창 크기 조절 완료")
|
||||||
|
|
||||||
|
self.set_window_position(self.whale_hwnd, 1, 1, 1280, 720) # 위치 (1, 1), 크기 (1280x720)
|
||||||
|
self.update_whale_rect()
|
||||||
|
|
||||||
# 주소창으로 이동 후 URL 입력
|
# 주소창으로 이동 후 URL 입력
|
||||||
pyautogui.hotkey('ctrl', 'l')
|
pyautogui.hotkey('ctrl', 'l')
|
||||||
|
|
@ -80,27 +98,27 @@ class WhaleTranslator:
|
||||||
self.failure_count = 0
|
self.failure_count = 0
|
||||||
self.logger.debug("실패 횟수가 초기화되었습니다.")
|
self.logger.debug("실패 횟수가 초기화되었습니다.")
|
||||||
|
|
||||||
def handle_translation_failure(self):
|
# def handle_translation_failure(self):
|
||||||
"""번역 실패 시 처리"""
|
# """번역 실패 시 처리"""
|
||||||
self.failure_count += 1
|
# self.failure_count += 1
|
||||||
self.logger.error(f"번역 실패! 실패 횟수: {self.failure_count}/{self.max_failures}")
|
# self.logger.error(f"번역 실패! 실패 횟수: {self.failure_count}/{self.max_failures}")
|
||||||
|
|
||||||
if self.failure_count >= self.max_failures:
|
# if self.failure_count >= self.max_failures:
|
||||||
self.logger.error("최대 실패 횟수에 도달했습니다. 웨일 브라우저를 재시작합니다.")
|
# self.logger.error("최대 실패 횟수에 도달했습니다. 웨일 브라우저를 재시작합니다.")
|
||||||
self.close_whale_window_if_exists()
|
# self.close_whale_window_if_exists()
|
||||||
time.sleep(2) # 재시작 전에 짧은 대기
|
# time.sleep(2) # 재시작 전에 짧은 대기
|
||||||
asyncio.run(self.start_whale_browser()) # 브라우저 재시작
|
# asyncio.run(self.start_whale_browser()) # 브라우저 재시작
|
||||||
self.reset_failures() # 실패 횟수 초기화
|
# self.reset_failures() # 실패 횟수 초기화
|
||||||
|
|
||||||
def is_image_in_clipboard_with_text(self):
|
# def is_image_in_clipboard_with_text(self):
|
||||||
"""클립보드에 이미지 데이터 또는 base64로 인코딩된 이미지 데이터가 있는지 확인"""
|
# """클립보드에 이미지 데이터 또는 base64로 인코딩된 이미지 데이터가 있는지 확인"""
|
||||||
clipboard_content = pyperclip.paste()
|
# clipboard_content = pyperclip.paste()
|
||||||
if clipboard_content.startswith("data:image") or isinstance(clipboard_content, bytes):
|
# if clipboard_content.startswith("data:image") or isinstance(clipboard_content, bytes):
|
||||||
self.logger.debug("클립보드에 이미지 데이터가 확인되었습니다.")
|
# self.logger.debug("클립보드에 이미지 데이터가 확인되었습니다.")
|
||||||
return True
|
# return True
|
||||||
else:
|
# else:
|
||||||
self.logger.debug("클립보드에 이미지 데이터가 없습니다.")
|
# self.logger.debug("클립보드에 이미지 데이터가 없습니다.")
|
||||||
return False
|
# return False
|
||||||
|
|
||||||
def is_image_in_clipboard(self):
|
def is_image_in_clipboard(self):
|
||||||
"""클립보드에 이미지 데이터가 있는지 확인"""
|
"""클립보드에 이미지 데이터가 있는지 확인"""
|
||||||
|
|
@ -118,10 +136,29 @@ class WhaleTranslator:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def find_whale_window(self):
|
def find_whale_window(self):
|
||||||
"""웨일 창 핸들을 찾는 메서드"""
|
"""웨일 창을 제목을 기준으로 찾는 메서드"""
|
||||||
if not self.whale_hwnd:
|
def callback(hwnd, extra):
|
||||||
self.whale_hwnd = self.find_window_by_title(self.whale_window_name)
|
if win32gui.IsWindowVisible(hwnd):
|
||||||
|
title = win32gui.GetWindowText(hwnd)
|
||||||
|
if self.whale_window_name in title:
|
||||||
|
extra.append(hwnd)
|
||||||
|
|
||||||
|
hwnd_list = []
|
||||||
|
win32gui.EnumWindows(callback, hwnd_list)
|
||||||
|
if hwnd_list:
|
||||||
|
self.whale_hwnd = hwnd_list[0]
|
||||||
|
self.logger.debug(f"웨일 창을 찾았습니다: {self.whale_hwnd}")
|
||||||
|
self.update_whale_rect()
|
||||||
return self.whale_hwnd
|
return self.whale_hwnd
|
||||||
|
else:
|
||||||
|
self.logger.debug("웨일 창을 찾지 못했습니다.")
|
||||||
|
return None
|
||||||
|
|
||||||
|
def update_whale_rect(self):
|
||||||
|
"""웨일 창의 위치 및 크기 rect를 업데이트"""
|
||||||
|
if self.whale_hwnd:
|
||||||
|
self.whale_rect = win32gui.GetWindowRect(self.whale_hwnd)
|
||||||
|
self.logger.debug(f"웨일 창 크기 및 위치 저장: {self.whale_rect}")
|
||||||
|
|
||||||
def find_window_by_title(self, window_name):
|
def find_window_by_title(self, window_name):
|
||||||
def enum_windows_callback(hwnd, result):
|
def enum_windows_callback(hwnd, result):
|
||||||
|
|
@ -225,47 +262,88 @@ class WhaleTranslator:
|
||||||
|
|
||||||
if not self.whale_hwnd:
|
if not self.whale_hwnd:
|
||||||
# 웨일 창을 찾지 못했을 경우 사용자에게 입력 받기
|
# 웨일 창을 찾지 못했을 경우 사용자에게 입력 받기
|
||||||
self.logger.debug("웨일 창을 찾지 못했습니다. 계속하려면 'y'를 입력하세요.")
|
self.logger.debug("웨일 창을 찾지 못했습니다. 새로운 웨일창 호출.")
|
||||||
self.create_and_update_whale_window()
|
self.create_and_update_whale_window()
|
||||||
|
|
||||||
if self.whale_hwnd:
|
if self.whale_hwnd:
|
||||||
try:
|
try:
|
||||||
|
self.logger.debug(f"웨일 창을 찾았습니다.{self.whale_hwnd}")
|
||||||
|
|
||||||
win32gui.ShowWindow(self.whale_hwnd, win32con.SW_RESTORE) # 웨일 창 활성화
|
win32gui.ShowWindow(self.whale_hwnd, win32con.SW_RESTORE) # 웨일 창 활성화
|
||||||
win32gui.SetForegroundWindow(self.whale_hwnd)
|
win32gui.SetForegroundWindow(self.whale_hwnd)
|
||||||
|
|
||||||
pyautogui.moveTo(960,580) # 마우스 센터로 이동
|
|
||||||
|
|
||||||
# pyautogui.hotkey('ctrl', 'l') # 웨일 브라우저의 주소창으로 이동
|
# pyautogui.hotkey('ctrl', 'l') # 웨일 브라우저의 주소창으로 이동
|
||||||
self.enter_url(url)
|
self.logger.debug(f"이미지 URL 주소 {url} 입력")
|
||||||
|
self.enter_url_for_clipboard(url)
|
||||||
pyautogui.press('enter')
|
pyautogui.press('enter')
|
||||||
# await asyncio.sleep(1) # 페이지 로딩 대기
|
time.sleep(2) # 페이지 로딩 대기
|
||||||
time.sleep(1)
|
|
||||||
|
|
||||||
pyautogui.rightClick()
|
# 현재 웨일 창의 해상도를 확인하여 기준 이하일 경우 패스
|
||||||
# await asyncio.sleep(0.2) # 페이지 로딩 대기
|
min_width = 200
|
||||||
time.sleep(1)
|
min_height = 150
|
||||||
|
|
||||||
pyautogui.press('r') # 번역 클릭
|
if not self.check_image_size(url, min_width=min_width,min_height=min_height):
|
||||||
# await asyncio.sleep(7) # 페이지 로딩 대기
|
self.logger.info("해상도가 기준보다 낮아 작업을 패스합니다.")
|
||||||
time.sleep(5)
|
return False # 해상도 기준 미달로 작업 종료
|
||||||
|
|
||||||
pyautogui.rightClick()
|
|
||||||
# await asyncio.sleep(0.2) # 페이지 로딩 대기
|
|
||||||
time.sleep(1)
|
|
||||||
|
|
||||||
pyautogui.press('c') # 번역된 이미지 클립보드에 복사
|
# 페이지 로딩 완료 대기
|
||||||
|
# self.logger.debug(f"페이지 로딩완료 대기")
|
||||||
|
# self.wait_for_loading_icon_to_disappear()
|
||||||
|
|
||||||
|
self.logger.debug(f"페이지 로딩 완료 후 웨일 창의 가운데로 마우스 커서 이동")
|
||||||
|
# pyautogui.moveTo(960,580) # 마우스 센터로 이동
|
||||||
|
self.move_mouse_to_center()
|
||||||
|
time.sleep(1) # 마우스 이동 후 대기
|
||||||
|
|
||||||
|
# original_color = self.get_mouse_position_color() # 현재 색상 가져오기
|
||||||
|
# self.colors['before'] = original_color
|
||||||
|
# self.logger.debug(f"번역 전 색상: {original_color}")
|
||||||
|
|
||||||
|
self.logger.debug("번역 작업을 위한 마우스 오른쪽 클릭 및 R 전송")
|
||||||
|
if not self.right_click_and_send_key('r'):
|
||||||
|
self.logger.error("번역 작업이 대화상자 문제로 중단되었습니다.")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# # 번역 성공 여부 확인
|
||||||
|
# result = self.check_translation_by_color_change()
|
||||||
|
# if result == "success":
|
||||||
|
# self.logger.debug("번역이 성공적으로 완료되었습니다!")
|
||||||
|
# elif result == "error":
|
||||||
|
# self.logger.debug("번역에 실패했습니다.")
|
||||||
|
# else:
|
||||||
|
# self.logger.debug("번역 상태를 확인하지 못했습니다.")
|
||||||
|
|
||||||
|
# # 최종 색상 출력
|
||||||
|
# self.logger.debug(f"번역 전 색상: {self.colors['before']}")
|
||||||
|
# self.logger.debug(f"번역 중 색상: {self.colors['during']}")
|
||||||
|
# self.logger.debug(f"번역 후 색상: {self.colors['after']}")
|
||||||
|
|
||||||
|
time.sleep(5) # 번역 대기
|
||||||
|
|
||||||
|
|
||||||
|
self.logger.debug("이미지 복사를 위한 마우스 오른쪽 클릭 및 C 전송")
|
||||||
|
if not self.right_click_and_send_key('c'):
|
||||||
|
self.logger.error("복사 작업이 대화상자 문제로 중단되었습니다.")
|
||||||
|
return False
|
||||||
|
|
||||||
|
self.logger.debug(f"클립보드에 번역된이미지 복사 대기 1s")
|
||||||
time.sleep(1) # 클립보드 업데이트 대기
|
time.sleep(1) # 클립보드 업데이트 대기
|
||||||
|
|
||||||
|
self.logger.debug("클립보드에 이미지 데이터가 존재하는지 확인 중.....")
|
||||||
if self.is_image_in_clipboard(): # 클립보드에 이미지 데이터가 있으면 성공
|
if self.is_image_in_clipboard(): # 클립보드에 이미지 데이터가 있으면 성공
|
||||||
self.translation_success_flag = True
|
self.translation_success_flag = True
|
||||||
self.logger.debug(f'번역 성공: {url}')
|
self.logger.info(f'번역 성공: {url}')
|
||||||
self.reset_failures() # 번역 성공 시 연속 실패 횟수 초기화
|
# self.reset_failures() # 번역 성공 시 연속 실패 횟수 초기화
|
||||||
else:
|
else:
|
||||||
self.logger.error(f'번역 실패: 클립보드에 이미지 데이터가 없음')
|
self.logger.error(f'번역 실패: 클립보드에 이미지 데이터가 없음')
|
||||||
self.handle_translation_failure()
|
# self.handle_translation_failure()
|
||||||
|
|
||||||
self.enter_url(self.newtab)
|
self.logger.info(f'번역 프로세스 완료. 웨일 기본페이지로 돌아감')
|
||||||
self.logger.debug(f'번역 완료: {url}')
|
# self.enter_url(self.newtab)
|
||||||
|
pyautogui.hotkey('alt', 'left') # 클립보드 이미지 붙여넣기
|
||||||
|
|
||||||
|
# self.logger.debug(f'번역 완료: {url}')
|
||||||
|
|
||||||
if self.vd_mode:
|
if self.vd_mode:
|
||||||
self.return_to_virtual_desktop_1()
|
self.return_to_virtual_desktop_1()
|
||||||
|
|
@ -275,12 +353,29 @@ class WhaleTranslator:
|
||||||
pass # 클립보드의 이미지를 path의 파일로 저장하고 저장경로를 리턴하는 메서드
|
pass # 클립보드의 이미지를 path의 파일로 저장하고 저장경로를 리턴하는 메서드
|
||||||
# path에는 현재 폴더의 tmp_img폴더에 상품명-옵션명 형태로 제공됨
|
# path에는 현재 폴더의 tmp_img폴더에 상품명-옵션명 형태로 제공됨
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.logger.error(f"번역 중 오류 발생: {e}")
|
self.logger.error(f"번역 중 오류 발생: {e}", exc_info=True)
|
||||||
self.handle_translation_failure()
|
return False
|
||||||
|
# self.handle_translation_failure()
|
||||||
else:
|
else:
|
||||||
self.logger.debug('웨일 창을 찾을 수 없습니다.')
|
self.logger.debug('웨일 창을 찾을 수 없습니다.')
|
||||||
self.handle_translation_failure()
|
return False
|
||||||
|
# self.handle_translation_failure()
|
||||||
|
|
||||||
|
def detect_unexpected_dialog(self):
|
||||||
|
"""예상치 못한 대화상자 감지"""
|
||||||
|
try:
|
||||||
|
dialog_hwnd = win32gui.FindWindow(None, "다른 이름으로 저장") # 저장 대화상자의 이름이 실제와 다를 수 있습니다.
|
||||||
|
if dialog_hwnd:
|
||||||
|
self.logger.debug("예상치 못한 '다른 이름으로 저장' 대화상자 발견!")
|
||||||
|
win32gui.PostMessage(dialog_hwnd, win32con.WM_CLOSE, 0, 0) # 대화상자 닫기
|
||||||
|
time.sleep(1)
|
||||||
|
return True
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.error(f"대화상자 감지 중 오류 발생: {e}", exc_info=True)
|
||||||
|
return False
|
||||||
|
|
||||||
def create_and_update_whale_window(self):
|
def create_and_update_whale_window(self):
|
||||||
"""
|
"""
|
||||||
|
|
@ -427,3 +522,263 @@ class WhaleTranslator:
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.logger.debug(f"가상 데스크톱 종료 중 오류 발생: {e}", exc_info=True)
|
self.logger.debug(f"가상 데스크톱 종료 중 오류 발생: {e}", exc_info=True)
|
||||||
|
|
||||||
|
# def wait_for_loading_icon_to_disappear(self, max_wait=10):
|
||||||
|
# """
|
||||||
|
# 로딩 아이콘이 화면에서 사라질 때까지 대기합니다.
|
||||||
|
# max_wait: 최대 대기 시간 (초)
|
||||||
|
# """
|
||||||
|
# start_time = time.time()
|
||||||
|
# while time.time() - start_time < max_wait:
|
||||||
|
# try:
|
||||||
|
# # 화면에서 아이콘 위치 확인 시도
|
||||||
|
# icon_location = pyautogui.locateOnScreen(self.page_loading_icon_path, confidence=0.9)
|
||||||
|
# if icon_location:
|
||||||
|
# self.logger.debug("페이지 로딩 중...")
|
||||||
|
# time.sleep(0.5) # 간격을 두고 다시 확인
|
||||||
|
|
||||||
|
# except pyautogui.ImageNotFoundException:
|
||||||
|
# # 아이콘이 화면에 없으면 로딩 완료로 간주
|
||||||
|
# self.logger.debug("페이지 로딩이 완료되었습니다.")
|
||||||
|
# return True # 로딩 완료
|
||||||
|
|
||||||
|
# self.logger.debug("로딩 완료 대기 시간이 초과되었습니다.")
|
||||||
|
# return False
|
||||||
|
|
||||||
|
def set_window_position(self, hwnd, x, y, width, height):
|
||||||
|
"""지정된 위치와 크기로 창을 조정"""
|
||||||
|
win32gui.ShowWindow(hwnd, win32con.SW_RESTORE)
|
||||||
|
win32gui.SetWindowPos(hwnd, None, x, y, width, height, win32con.SWP_NOZORDER | win32con.SWP_NOACTIVATE)
|
||||||
|
self.logger.debug(f"창 위치 및 크기 설정: 위치({x}, {y}), 크기({width}x{height})")
|
||||||
|
|
||||||
|
def get_whale_window_title(self):
|
||||||
|
"""
|
||||||
|
현재 활성화된 웨일 창의 이름을 가져옵니다.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
if self.whale_hwnd:
|
||||||
|
window_title = win32gui.GetWindowText(self.whale_hwnd)
|
||||||
|
self.logger.debug(f"현재 웨일 창의 제목: {window_title}")
|
||||||
|
else:
|
||||||
|
self.logger.warning(f"웨일 창 핸들이 없습니다.")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
window_title = None
|
||||||
|
self.logger.error(f"현재 웨일 창의 제목을 가져올 수 없습니다. : {e}", exc_info = True)
|
||||||
|
|
||||||
|
return window_title
|
||||||
|
|
||||||
|
def check_image_size(self, url, min_width=200, min_height=150):
|
||||||
|
"""
|
||||||
|
현재 활성화된 웨일 창의 이름을 가져온 후
|
||||||
|
제목에서 이미지의 해상도를 확인한 후, 일정 크기 이하인 경우 작업을 패스하도록 처리합니다.
|
||||||
|
"""
|
||||||
|
window_title = self.get_whale_window_title()
|
||||||
|
|
||||||
|
# 해상도를 추출하기 위해 제목에서 괄호 안의 (숫자×숫자) 부분을 찾음
|
||||||
|
|
||||||
|
if not window_title == self.whale_window_name:
|
||||||
|
match = re.search(r"\((\d+)×(\d+)\)", window_title)
|
||||||
|
if match:
|
||||||
|
width = int(match.group(1))
|
||||||
|
height = int(match.group(2))
|
||||||
|
self.logger.debug(f"이미지 해상도: {width}×{height}")
|
||||||
|
|
||||||
|
# 해상도가 기준보다 작은 경우 패스
|
||||||
|
if width < min_width or height < min_height:
|
||||||
|
self.logger.debug(f"이미지 해상도가 기준이하 [{min_width} x {min_height}]입니다. 작업을 패스합니다.")
|
||||||
|
return False # 작업을 수행하지 않음
|
||||||
|
return True # 작업을 계속 진행
|
||||||
|
|
||||||
|
elif window_title == self.whale_window_name:
|
||||||
|
self.logger.warning("이미지 주소로 이동하지 못하여 이미지 해상도를 가져오지 못했습니다.")
|
||||||
|
self.enter_url(url)
|
||||||
|
window_title = self.get_whale_window_title()
|
||||||
|
match = re.search(r"\((\d+)×(\d+)\)", window_title)
|
||||||
|
if match:
|
||||||
|
width = int(match.group(1))
|
||||||
|
height = int(match.group(2))
|
||||||
|
self.logger.debug(f"이미지 해상도: {width}×{height}")
|
||||||
|
|
||||||
|
# 해상도가 기준보다 작은 경우 패스
|
||||||
|
if width < min_width or height < min_height:
|
||||||
|
self.logger.debug(f"이미지 해상도가 기준이하 [{min_width} x {min_height}]입니다. 작업을 패스합니다.")
|
||||||
|
return False # 작업을 수행하지 않음
|
||||||
|
return True # 작업을 계속 진행
|
||||||
|
|
||||||
|
else:
|
||||||
|
return False
|
||||||
|
|
||||||
|
self.logger.error("웨일 창을 찾을 수 없습니다.")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def move_mouse_to_center(self):
|
||||||
|
"""웨일 브라우저 창의 중앙으로 마우스 커서를 이동"""
|
||||||
|
if self.whale_rect:
|
||||||
|
center_x = (self.whale_rect[0] + self.whale_rect[2]) // 2 # 가로 중앙 계산
|
||||||
|
center_y = (self.whale_rect[1] + self.whale_rect[3]) // 2 # 세로 중앙 계산
|
||||||
|
|
||||||
|
self.logger.debug(f"마우스 커서를 추가로 50px 내림")
|
||||||
|
center_y = center_y + 50
|
||||||
|
|
||||||
|
pyautogui.moveTo(center_x, center_y)
|
||||||
|
self.logger.debug(f"마우스 커서를 창 중앙으로 이동: ({center_x}, {center_y})")
|
||||||
|
else:
|
||||||
|
self.logger.error("웨일 창의 크기를 알 수 없습니다. 먼저 창을 찾으세요.")
|
||||||
|
|
||||||
|
def check_translation_by_color_change(self):
|
||||||
|
start_time = time.time()
|
||||||
|
|
||||||
|
# 번역 시작 감지 (translating.png 확인)
|
||||||
|
while time.time() - start_time < self.timeout:
|
||||||
|
current_color = self.get_mouse_position_color()
|
||||||
|
self.logger.debug(f"현재 색상: {current_color}")
|
||||||
|
|
||||||
|
# 필터가 사라져서 색상이 변했는지 확인
|
||||||
|
if self.is_color_changed(current_color):
|
||||||
|
self.colors['after'] = current_color
|
||||||
|
self.logger.debug("색상 변화 감지 (필터 제거됨)")
|
||||||
|
|
||||||
|
# translating.png가 여전히 존재하는지 확인하여 번역 성공 여부 판단
|
||||||
|
result = self.find_image_with_confidence(self.translating_image_path, confidence=0.4)
|
||||||
|
if result:
|
||||||
|
return "success"
|
||||||
|
else:
|
||||||
|
return "error"
|
||||||
|
else:
|
||||||
|
# 번역 실패 이미지 확인
|
||||||
|
if self.is_translation_failed():
|
||||||
|
self.colors['after'] = current_color
|
||||||
|
return "error"
|
||||||
|
|
||||||
|
time.sleep(self.pixel_check_interval)
|
||||||
|
|
||||||
|
# 타임아웃 발생 시 translating.png 여부로 번역 성공/실패 판단
|
||||||
|
self.colors['after'] = current_color
|
||||||
|
result = self.find_image_with_confidence(self.translating_image_path, confidence=0.4)
|
||||||
|
if result:
|
||||||
|
self.logger.debug("번역 성공으로 간주 (타임아웃 후 translating.png 존재)")
|
||||||
|
return "success"
|
||||||
|
else:
|
||||||
|
self.logger.debug("번역 실패로 간주 (타임아웃 후 translating.png 없음)")
|
||||||
|
return "error"
|
||||||
|
|
||||||
|
def find_image_with_confidence(self, image_path, confidence=0.8):
|
||||||
|
"""이미지를 찾을 때 highest confidence 값을 로그로 출력하는 메서드"""
|
||||||
|
if self.whale_rect:
|
||||||
|
# 웨일 창의 크기를 region으로 설정
|
||||||
|
region = (self.whale_rect[0], self.whale_rect[1],
|
||||||
|
self.whale_rect[2] - self.whale_rect[0],
|
||||||
|
self.whale_rect[3] - self.whale_rect[1])
|
||||||
|
self.logger.debug(f"이미지를 찾을 영역: {region}")
|
||||||
|
|
||||||
|
try:
|
||||||
|
# locateOnScreen 시도
|
||||||
|
result = pyscreeze.locateOnScreen(image_path, confidence=confidence, region=region)
|
||||||
|
|
||||||
|
if result:
|
||||||
|
self.logger.debug(f"이미지를 찾았습니다: {image_path} confidence: {confidence}")
|
||||||
|
return result
|
||||||
|
else:
|
||||||
|
# locateOnScreen 함수가 이미지와 충분히 유사하다고 판단하지 않은 경우
|
||||||
|
highest_confidence = pyscreeze._locateAll_opencv(image_path, region)[0][1] # 최고 유사도 값 가져오기
|
||||||
|
self.logger.debug(f"최고 유사도 값: {highest_confidence}")
|
||||||
|
|
||||||
|
if highest_confidence >= confidence:
|
||||||
|
self.logger.debug(f"유사한 이미지를 찾았습니다. 최고 유사도 값: {highest_confidence}")
|
||||||
|
return True # 유사도는 충분하므로 True 반환
|
||||||
|
else:
|
||||||
|
self.logger.error(f"{image_path} 이미지를 찾지 못했습니다. 유사도: {highest_confidence}")
|
||||||
|
return None # 유사도가 충분하지 않으면 None 반환
|
||||||
|
|
||||||
|
except pyscreeze.ImageNotFoundException as e:
|
||||||
|
self.logger.error(f"{image_path} 이미지를 찾지 못했습니다. 예외 발생: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
# def check_translation_by_color_change_ori(self, original_color):
|
||||||
|
# start_time = time.time()
|
||||||
|
# while time.time() - start_time < self.timeout:
|
||||||
|
# current_color = self.get_mouse_position_color()
|
||||||
|
# if self.colors['during'] is None: # 번역 중 첫 색상 기록
|
||||||
|
# self.colors['during'] = current_color
|
||||||
|
# self.logger.debug(f"현재 색상: {current_color}")
|
||||||
|
|
||||||
|
# if self.is_similar_color(current_color, original_color):
|
||||||
|
# self.colors['after'] = current_color
|
||||||
|
# self.logger.debug("번역 성공 감지 (원래 색상과 유사)")
|
||||||
|
# return "success"
|
||||||
|
|
||||||
|
# if not self.is_similar_color(current_color, original_color):
|
||||||
|
# self.logger.debug("번역 중 상태 감지 (필터 색상)")
|
||||||
|
|
||||||
|
# if self.is_translation_failed():
|
||||||
|
# self.colors['after'] = current_color
|
||||||
|
# return "error"
|
||||||
|
|
||||||
|
# time.sleep(self.pixel_check_interval)
|
||||||
|
|
||||||
|
# # 타임아웃 발생 시, 번역 성공으로 간주
|
||||||
|
# self.colors['after'] = current_color
|
||||||
|
# self.logger.debug("번역 성공으로 간주 (타임아웃)")
|
||||||
|
# return "success"
|
||||||
|
|
||||||
|
def is_color_changed(self, current_color):
|
||||||
|
"""현재 색상이 변화했는지 확인 (필터 제거 여부)"""
|
||||||
|
if self.colors['during'] is None:
|
||||||
|
self.colors['during'] = current_color
|
||||||
|
return not self.is_similar_color(current_color, self.colors['during'])
|
||||||
|
|
||||||
|
def is_similar_color(self, color1, color2):
|
||||||
|
"""색상이 유사한지 확인 (허용 오차 적용)"""
|
||||||
|
r_diff = abs(color1[0] - color2[0])
|
||||||
|
g_diff = abs(color1[1] - color2[1])
|
||||||
|
b_diff = abs(color1[2] - color2[2])
|
||||||
|
return r_diff < self.color_tolerance and g_diff < self.color_tolerance and b_diff < self.color_tolerance
|
||||||
|
|
||||||
|
def get_mouse_position_color(self):
|
||||||
|
x, y = pyautogui.position()
|
||||||
|
return pyautogui.pixel(x, y)
|
||||||
|
|
||||||
|
def is_translation_failed(self):
|
||||||
|
"""번역 실패 이미지 확인"""
|
||||||
|
for image_path in self.error_image_paths:
|
||||||
|
try:
|
||||||
|
# if pyautogui.locateOnScreen(image_path, confidence=0.8):
|
||||||
|
if self.find_image_with_confidence(self.translating_image_path, confidence=0.8):
|
||||||
|
self.logger.error(f"번역 실패: '{os.path.basename(image_path)}' 메시지가 감지되었습니다.")
|
||||||
|
return True
|
||||||
|
except pyautogui.ImageNotFoundException:
|
||||||
|
self.logger.debug(f"번역 실패 이미지가 화면에 없습니다: {os.path.basename(image_path)}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def right_click_and_send_key(self, key, max_retries=3):
|
||||||
|
"""
|
||||||
|
마우스 오른쪽 클릭 후 지정된 키를 전송하는 메서드.
|
||||||
|
대화상자가 감지되면 닫고, 최대 재시도 횟수까지 반복 시도.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
key (str): 전송할 키 ('r' 또는 'c').
|
||||||
|
max_retries (int): 최대 재시도 횟수.
|
||||||
|
"""
|
||||||
|
retry_count = 0
|
||||||
|
|
||||||
|
while retry_count < max_retries:
|
||||||
|
self.logger.debug(f"마우스 오른쪽 클릭 시도 #{retry_count + 1}")
|
||||||
|
pyautogui.rightClick()
|
||||||
|
time.sleep(1) # 컨텍스트 창 대기
|
||||||
|
|
||||||
|
if self.detect_unexpected_dialog():
|
||||||
|
self.logger.debug("예상치 못한 대화상자를 닫고 다시 시도합니다.")
|
||||||
|
retry_count += 1
|
||||||
|
else:
|
||||||
|
self.logger.debug(f"대화상자가 감지되지 않았습니다. 키보드로 '{key}' 전송 준비.")
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
self.logger.error(f"대화상자가 계속 발생하여 '{key}' 작업을 중단합니다.")
|
||||||
|
return False # 작업 실패
|
||||||
|
|
||||||
|
# 대화상자가 없으면 지정된 키를 전송
|
||||||
|
self.logger.debug(f"키보드로 '{key}' 전송")
|
||||||
|
pyautogui.press(key)
|
||||||
|
time.sleep(0.5) # 작업 대기
|
||||||
|
return True # 작업 성공
|
||||||
|
|
|
||||||