This commit is contained in:
Envy_PC 2024-10-19 20:38:18 +09:00
commit 41a40bf527
37 changed files with 448390 additions and 86606 deletions

Binary file not shown.

Binary file not shown.

Binary file not shown.

File diff suppressed because one or more lines are too long

77589
appTranslator.log.1 Normal file

File diff suppressed because one or more lines are too long

79943
appTranslator.log.2 Normal file

File diff suppressed because one or more lines are too long

81994
appTranslator.log.3 Normal file

File diff suppressed because it is too large Load Diff

81303
appTranslator.log.4 Normal file

File diff suppressed because it is too large Load Diff

79949
appTranslator.log.5 Normal file

File diff suppressed because one or more lines are too long

View File

@ -116,10 +116,10 @@ class BrowserController:
# 기본 페이지가 없을 수 있으므로 새로운 페이지 생성
self.page = await self.browser.new_page()
self.logger.debug('새 페이지 로딩 중...')
self.logger.info('새 페이지 로딩 중...')
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:
@ -132,7 +132,7 @@ class BrowserController:
# 창 핸들 찾기 (동적으로 얻은 페이지 제목 사용)
self.chrome_hwnd = self.find_window_by_title(page_title)
if not self.chrome_hwnd:
self.logger.debug('크롬 창을 찾을 수 없습니다.')
self.logger.warning('크롬 창을 찾을 수 없습니다.')
else:
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):
"""로그인 처리"""
self.logger.debug(f'로그인 시도 중: {"관리자" if is_admin else "직원"} 계정')
self.logger.info(f'로그인 시도 중: {"관리자" if is_admin else "직원"} 계정')
if is_admin:
# 관리자 로그인 처리
@ -158,7 +158,7 @@ class BrowserController:
await self.page.fill(self.login_password_locator, user_password)
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)
@ -169,7 +169,7 @@ class BrowserController:
if self.browser:
await self.browser.close()
await self.playwright.stop()
self.logger.debug('브라우저 종료됨.')
self.logger.info('브라우저 종료됨.')
def find_window_by_title(self, window_name):
"""창 제목을 통해 핸들을 찾는 메서드"""
@ -187,7 +187,7 @@ class BrowserController:
win32gui.SetForegroundWindow(self.chrome_hwnd)
self.logger.debug('크롬 창으로 포커스 이동.')
else:
self.logger.debug('크롬 창을 찾을 수 없습니다.')
self.logger.error('크롬 창을 찾을 수 없습니다.')
async def get_total_product_count_ori(self):
@ -219,107 +219,99 @@ class BrowserController:
}}''', self.total_product_count_locator)
if element_text:
self.logger.debug(f"총 상품수 확인: {element_text}") # 텍스트 확인용 로그
self.logger.info(f"총 상품수 확인: {element_text}") # 텍스트 확인용 로그
# "총 xx개 상품"에서 숫자만 추출
count = int(''.join(filter(str.isdigit, element_text)))
return count
else:
self.logger.debug("요소를 찾을 수 없습니다.")
self.logger.warning("요소를 찾을 수 없습니다.")
return 0
except Exception as e:
self.logger.debug(f"상품 수를 가져오는 중 오류 발생: {e}", exc_info=True)
self.logger.error(f"상품 수를 가져오는 중 오류 발생: {e}", exc_info=True)
return 0
# async def get_product_name(self, index, selector='xpath'):
# def fetch_image_urls_ori(self, html_content):
# """
# 상품명을 수집하는 메서드
# index : 상품명을 수집하는 인덱스
# selector : 수집방법 (css 또는 xpath)
# HTML 콘텐츠에서 모든 <img> 태그의 URL을 순서대로 추출하고 중복 제거.
# """
# try:
# # config.ini에서 설정된 선택자에 인덱스를 적용하여 가져옴
# # product_name_selector = self.product_name_template.format(index=index)
# # self.logger.debug(f"사용된 선택자: {product_name_selector}") # 선택자 출력
# soup = BeautifulSoup(html_content, 'html.parser')
# 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 기반으로 요소 검색
# product_name_element = await self.page.locator(f"xpath={product_name_xpath_selector}").element_handle()
# # product_name_element = await self.page.query_selector(product_name_selector)
# # product_name_element가 None인지 확인
# if product_name_element is None:
# self.logger.error(f"상품명 요소를 찾을 수 없습니다: index {index}")
# 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 "수집 오류 발생"
# # class="image_resized"를 가진 모든 <img> 태그 찾기
# images_resized = soup.find_all('img', class_='image_resized')
# for img in images_resized:
# if img and 'src' in img.attrs:
# url = img['src']
# if url not in seen_urls:
# image_urls.append(url)
# seen_urls.add(url) # 중복 방지
# return image_urls
def fetch_image_urls(self, html_content):
"""
HTML 콘텐츠에서 모든 <img> 태그의 URL을 순서대로 추출하고 중복 제거.
HTML 콘텐츠에서 모든 <img> 태그의 URL을 추출하는 함수.
<figure> 안의 <img> 태그와 독립된 <img> 태그 모두 처리.
"""
soup = BeautifulSoup(html_content, 'html.parser')
# 모든 <img> 태그를 찾기
image_urls = []
img_tags = soup.find_all('img')
for img in img_tags:
# img 태그에서 src 속성 추출
if 'src' in img.attrs:
image_url = img['src']
image_urls.append(image_url)
self.logger.debug(f"fetch_image_urls 에서 추출한 이미지URL 갯수 : {len(image_urls)}")
# 중복된 이미지를 제거하기 위해 set 사용
image_urls_set = set()
# class="image_resized"를 가진 모든 <img> 태그 찾기
images_resized = soup.find_all('img', class_='image_resized')
for img in images_resized:
if img and 'src' in img.attrs:
image_urls_set.add(img['src']) # 중복을 방지하기 위해 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:
image_urls_set.add(img_tag['src']) # 중복을 방지하기 위해 set에 추가
# set을 list로 변환하여 반환 (순서 유지가 필요하면 set 대신 리스트로 처리해야 함)
image_urls = list(image_urls_set)
self.logger.debug(f"fetch_image_urls 에서 추출한 이미지URL 목록 : {image_urls}")
return image_urls
async def close_ad_if_exists(self):
"""광고 다이얼로그가 있으면 닫기 버튼을 클릭하는 메서드"""
try:
# 광고 다이얼로그가 나타날 때까지 기다림
await self.page.wait_for_selector(self.close_ad_dialog_locator, timeout=3000)
self.logger.debug("다이얼로그가 발견되었습니다. 닫기 버튼을 클릭합니다.")
await self.page.wait_for_selector(self.close_ad_dialog_locator, timeout=3000, state='visible')
self.logger.info("다이얼로그가 발견되었습니다. 닫기 버튼을 클릭합니다.")
# 닫기 버튼 클릭
close_button = await self.page.query_selector(self.close_ad_button_locator)
if close_button:
await close_button.click()
self.logger.debug("다이얼로그를 성공적으로 닫았습니다.")
self.logger.info("다이얼로그를 성공적으로 닫았습니다.")
else:
self.logger.debug("닫기 버튼을 찾지 못했습니다.")
self.logger.warning("닫기 버튼을 찾지 못했습니다.")
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):
"""신규 상품 등록 페이지로 이동"""
try:
new_product_page_locator = self.locator_manager.get_locator('BrowserControl', 'new_product_page_locator')
await self.page.click(new_product_page_locator)
self.logger.debug("신규 상품 등록 페이지로 이동 완료.")
self.logger.info("신규 상품 등록 페이지로 이동 완료.")
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):
"""현재 페이지의 세부사항 수정 및 업로드 버튼을 찾기"""
@ -328,7 +320,7 @@ class BrowserController:
edit_button_selector = self.product_edit_button
if not edit_button_selector:
self.logger.debug("상품 수정 버튼의 선택자를 찾을 수 없습니다.")
self.logger.warning("상품 수정 버튼의 선택자를 찾을 수 없습니다.")
return []
# 선택자를 사용해 버튼 객체를 찾음
@ -336,17 +328,17 @@ class BrowserController:
# 버튼이 존재하는지 확인
if await buttons.count() == 0:
self.logger.debug("세부사항 수정 및 업로드 버튼을 찾을 수 없습니다.")
self.logger.warning("세부사항 수정 및 업로드 버튼을 찾을 수 없습니다.")
return []
count = await buttons.count()
self.logger.debug(f"수정할 상품 개수: {count}")
self.logger.info(f"수정할 상품 개수: {count}")
# 모든 버튼을 리스트로 반환
return [buttons.nth(i) for i in range(count)]
except Exception as e:
self.logger.debug(f"상품 수정 버튼을 찾는 중 오류: {e}", exc_info=True)
self.logger.error(f"상품 수정 버튼을 찾는 중 오류: {e}", exc_info=True)
return []
async def get_product_edit_buttons_by_templete(self):
@ -361,16 +353,16 @@ class BrowserController:
# 버튼이 존재하는지 확인
button_count = await buttons.count()
if button_count == 0:
self.logger.debug("세부사항 수정 및 업로드 버튼을 찾을 수 없습니다.")
self.logger.warning("세부사항 수정 및 업로드 버튼을 찾을 수 없습니다.")
return []
self.logger.debug(f"현재 페이지의 수정할 상품 개수: {button_count}")
self.logger.info(f"현재 페이지의 수정할 상품 개수: {button_count}")
# 모든 버튼을 리스트로 반환
return [buttons.nth(i) for i in range(button_count)]
except Exception as e:
self.logger.debug(f"상품 수정 버튼을 찾는 중 오류: {e}", exc_info=True)
self.logger.error(f"상품 수정 버튼을 찾는 중 오류: {e}", exc_info=True)
return []
@ -387,11 +379,11 @@ class BrowserController:
if button:
await button.scroll_into_view_if_needed()
await self.page.evaluate('arguments[0].click();', button)
self.logger.debug(f'{index}번째 상품의 수정 버튼 클릭 완료')
self.logger.info(f'{index}번째 상품의 수정 버튼 클릭 완료')
else:
self.logger.debug(f'{index}번째 상품의 수정 버튼을 찾지 못했습니다.')
self.logger.warning(f'{index}번째 상품의 수정 버튼을 찾지 못했습니다.')
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):
@ -402,42 +394,42 @@ class BrowserController:
self.logger.debug("상품의 '세부사항 수정 및 업로드' 버튼을 화면에 보이도록 스크롤.")
await button.click()
self.logger.debug("세부사항 수정 다이얼로그 열기 완료.")
self.logger.info("세부사항 수정 다이얼로그 열기 완료.")
await self.page.wait_for_selector('div.ant-tabs-nav') # 다이얼로그가 완전히 로딩될 때까지 기다림
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):
"""상세페이지 탭 클릭"""
try:
await self.page.click(self.detail_tab_locator)
self.logger.debug("상세페이지 탭 클릭 완료.")
self.logger.info("상세페이지 탭 클릭 완료.")
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):
"""옵션 탭 클릭"""
try:
await self.page.click(self.option_tab_locator)
self.logger.debug("옵션 탭 클릭 완료.")
self.logger.info("옵션 탭 클릭 완료.")
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):
"""가격 탭 클릭"""
try:
await self.page.click(self.price_tab_locator)
self.logger.debug("가격 탭 클릭 완료.")
self.logger.info("가격 탭 클릭 완료.")
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):
"""상품명 탭 클릭"""
try:
await self.page.click(self.title_tab_locator)
self.logger.debug("상품명 탭 클릭 완료.")
self.logger.info("상품명 탭 클릭 완료.")
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):
"""상세페이지에서 이미지 URL 추출"""
@ -452,13 +444,13 @@ class BrowserController:
# '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")
# HTML 소스에서 이미지 URL 추출
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 삭제
self.logger.debug('img 태그를 삭제 중...')
@ -479,12 +471,18 @@ class BrowserController:
if is_option_data:
self.logger.debug('옵션 데이터 입력 시작')
option_data = {} # option_data 초기화
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(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')
# 선두부 텍스트 입력
@ -493,79 +491,86 @@ class BrowserController:
if 'leading_text' in key and leading_text: # leading_text 항목만 가져오기
await input_field.type(leading_text)
await input_field.press('Enter')
self.logger.debug(f"{key} 텍스트 입력 완료: {leading_text}")
self.logger.info(f"{key} 텍스트 입력 완료: {leading_text}")
# 각 옵션을 한 줄씩 입력
await input_field.press('Enter')
await input_field.type("# > 옵션 목록")
await input_field.press('Enter')
await input_field.press('Enter')
if not is_single:
self.logger.info('단일옵션이 아니므로 옵션목록을 입력')
# 각 옵션을 한 줄씩 입력
await input_field.type("# 옵션 목록")
await input_field.press('Enter')
# 첫 번째 옵션에만 - 기호를 붙여 목록 시작
await input_field.type(f"- A. {option_data[0]}")
await input_field.press('Enter') # 첫 번째 옵션 이후 엔터로 줄바꿈
# 첫 번째 옵션의 번역된 옵션명만 입력
first_key = list(option_data.keys())[0]
first_value = option_data[first_key]
await input_field.type(f"- 1. {first_value}")
await input_field.press('Enter') # 첫 번째 옵션 이후 엔터로 줄바꿈
# 나머지 옵션들은 - 없이 입력하여 마크다운 목록으로 표시
for i, option in enumerate(option_data[1:], start=2):
option_text = option[0] if isinstance(option, tuple) else option
option_prefix = f"{chr(64 + i)}. "
await input_field.type(option_prefix + option_text)
await input_field.press('Enter') # 엔터 키를 입력하여 줄바꿈
# 나머지 옵션도 번역된 옵션명만 입력
for i, (key, value) in enumerate(list(option_data.items())[1:], start=2):
await input_field.type(f"{i}. {value}") # 옵션 번호와 번역된 옵션명만 입력
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.press('Enter')
await input_field.type('---')
await input_field.press('Enter')
# 후두부 텍스트 입력
await input_field.type('---')
await input_field.type('### 나열된 옵션목록 이외의 옵션이 필요하실 경우 고객센터로 연락주세요.')
await input_field.press('Enter')
await input_field.type('---')
await input_field.press('Enter')
self.logger.debug('옵션 데이터 입력 완료 후 엔터 입력')
self.logger.info('옵션 데이터 입력 완료')
return image_urls
except Exception as e:
self.logger.debug(f"이미지 URL 추출 중 오류: {e}", exc_info=True)
return []
self.logger.error(f"이미지 URL 추출 & 옵션데이터 입력 처리 중 오류: {e}", exc_info=True)
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("크롬으로 포커스를 옮기고 클립보드의 이미지를 붙여넣고 엔터 입력")
try:
self.switch_to_chrome() # 크롬으로 포커스 이동
await clipboardImageManager.process_clipboard(url) # 클립보드 내용을 처리
clipboardImageManager.process_clipboard(url) # 클립보드 내용을 처리
# clipboard_content = pyperclip.paste()
if clipboardImageManager.is_clipboard_image():
pyautogui.hotkey('ctrl', 'v') # 클립보드 이미지 붙여넣기
self.logger.info("이미지 붙여넣기 완료.")
pyautogui.press('right') # 오른쪽 입력
<<<<<<< HEAD
self.logger.debug("이미지 붙여넣기 완료.")
self.logger.debug("이미지 붙여넣기 완료로 클립보드 비우기.")
clipboardImageManager.clear_clipboard()
=======
self.logger.info("이미지 붙여넣기 완료로 클립보드 비우기.")
clipboardImageManager.clear_clipboard()
return True
>>>>>>> f71275020a91aa828fd528172caa46a2bd494578
else:
self.logger.debug("클립보드가 비어있습니다.")
self.logger.warning("클립보드가 비어있습니다.")
return False
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):
"""상품 수정 후 저장 버튼 클릭"""
try:
await self.page.click(self.save_button_locator)
await self.page.keyboard.press("Escape")
self.logger.debug("상품 수정 내용 저장 및 ECS 완료.")
self.logger.info("상품 수정 내용 저장 및 ECS 완료.")
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):
"""상품 수정 후 저장 버튼 클릭"""
try:
await self.page.click(self.save_button_locator)
self.logger.debug("상품 수정 내용 저장 완료.")
self.logger.info("상품 수정 내용 저장 완료.")
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):
"""다음 페이지로 이동"""
@ -574,7 +579,7 @@ class BrowserController:
current_page = await self.page.query_selector(self.current_page_locator)
if not current_page:
self.logger.debug("현재 페이지 정보를 찾을 수 없습니다.")
self.logger.warning("현재 페이지 정보를 찾을 수 없습니다.")
return False
# 현재 활성화된 페이지 번호를 가져옴
@ -588,14 +593,14 @@ class BrowserController:
if next_page_button:
await next_page_button.click() # 페이지 버튼 클릭
# await self.page.wait_for_load_state('domcontentloaded') # 페이지 로딩이 완료될 때까지 대기
self.logger.debug(f"페이지 {next_page_number}로 이동 완료.")
self.logger.info(f"페이지 {next_page_number}로 이동 완료.")
return True
else:
self.logger.debug("다음 페이지가 없습니다.")
self.logger.warning("다음 페이지가 없습니다.")
return False
except Exception as e:
self.logger.debug(f"다음 페이지로 이동 중 오류 발생: {e}", exc_info=True)
self.logger.error(f"다음 페이지로 이동 중 오류 발생: {e}", exc_info=True)
return False
def switch_to_chrome(self):
@ -608,9 +613,9 @@ class BrowserController:
win32gui.SetForegroundWindow(self.chrome_hwnd)
self.logger.debug('크롬 창으로 포커스 이동.')
else:
self.logger.debug('크롬 창을 찾을 수 없습니다.')
self.logger.warning('크롬 창을 찾을 수 없습니다.')
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):
"""
@ -734,7 +739,7 @@ class BrowserController:
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")
while True:
@ -744,11 +749,11 @@ class BrowserController:
if current_height == previous_height:
break # 더 이상 스크롤할 내용이 없으면 종료
previous_height = current_height
self.logger.debug('페이지 스크롤 완료.')
self.logger.info('페이지 스크롤 완료.')
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")
while previous_height > 0:
@ -759,4 +764,4 @@ class BrowserController:
break # 더 이상 스크롤할 내용이 없으면 종료
previous_height = current_height
self.logger.debug('페이지 위로 스크롤 완료.')
self.logger.info('페이지 위로 스크롤 완료.')

View File

@ -20,7 +20,11 @@ class ClipboardImageManager:
self.browser_controller = browser_controller # BrowserController 인스턴스를 전달받음
self.debug = debug # 디버그 플래그를 클래스 변수로 사용
<<<<<<< HEAD
# self.debug = True
=======
self.debug = False
>>>>>>> f71275020a91aa828fd528172caa46a2bd494578
def get_clipboard_data(self):
"""클립보드의 텍스트 또는 이미지 데이터를 가져옵니다."""
@ -144,7 +148,7 @@ class ClipboardImageManager:
self.logger.debug("유효하지 않은 Base64 이미지 데이터입니다.")
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 이미지 객체로 반환"""
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",
@ -179,16 +183,19 @@ class ClipboardImageManager:
else:
self.logger.debug(f"이미지 로딩 실패, HTTP 상태 코드: {response.status_code}. 재시도 {retries + 1}/{max_retries}")
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:
self.logger.debug(f"이미지 로딩 중 오류 발생: {e}. 재시도 {retries + 1}/{max_retries}")
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("이미지 다운로드 최대 재시도 횟수를 초과했습니다.")
return None
async def process_clipboard(self, original_url, path=None):
def process_clipboard(self, original_url, path=None):
"""클립보드의 내용을 처리하고, 필요한 경우 이미지 변환, 크롭 또는 클립보드 비우기"""
try:
@ -248,7 +255,7 @@ class ClipboardImageManager:
self.logger.info("[process_clipboard] 클립보드에 이미지 없음")
if original_url:
image = await self.download_image_from_url(original_url)
image = self.download_image_from_url(original_url)
if image:
self.logger.debug("원본 이미지 다운로드 성공, 클립보드에 저장 중...")
self.set_image_to_clipboard(image) # 크롭 없이 저장
@ -266,7 +273,13 @@ class ClipboardImageManager:
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):
"""클립보드에서 이미지를 가져오는 함수"""

View File

@ -7,7 +7,8 @@ oversea_shipping_locator = '//*[@id='productMainContentContainerId']/div/div[1]/
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'
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]
# 옵션 관련 선택자
@ -18,18 +19,22 @@ option_product_locator = '//div[@id="productMainContentContainerId"]//label[cont
total_options_selector = '#productMainContentContainerId label.ant-checkbox-wrapper'
; 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'
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'
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"]'
image_selector_template = '#productMainContentContainerId li:nth-child({index}) img.sc-gbvfcU.ezktkd'
price_selector_template = '#productMainContentContainerId li:nth-child({index}) sup'
; image_selector_template = '#productMainContentContainerId li:nth-child({index}) img.sc-gbvfcU.ezktkd'
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'
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'
file_input_locator = 'input[type="file"]'
low_order_button_locator = 'button:has-text("가격 낮은 순")'
AtoZ_button_locator = 'button:has-text("A-Z")'
one_to_nine_button_locator = 'button:has-text("1-99")'
[DetailLocators]
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"]'
# 광고 다이얼로그 관련 선택자
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'
# 상품 관련 선택자

22
gui.py
View File

@ -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.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.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.running = False
@ -72,6 +72,7 @@ class TranslationApp(QWidget):
self.price_count = 0
self.detail_image_count = 0
self.thumb_image_count = 0
self.current_options_info = {}
self.current_stage_index = 0 # 현재 진행 중인 단계 인덱스
@ -715,7 +716,7 @@ class TranslationApp(QWidget):
self.logger.debug(f'현재 페이지: {page_number}')
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페이지가 아니므로 동적로딩을 위해 휠 스크롤 업')
# 4. 현재 페이지의 모든 "세부사항 수정 및 업로드" 버튼 찾기
@ -842,10 +843,16 @@ class TranslationApp(QWidget):
self.logger.debug('번역 작업이 중단되었습니다.')
break
self.logger.debug(f"이미지 번역 프로세스")
self.whale_translator.translate_image(url)
self.logger.debug(f"이미지 붙여넣기")
await self.browser_controller.paste_image_in_chrome(self.clipboardImageManager, url)
self.logger.debug(f"웨일 브라우저를 활용한 이미지 번역 프로세스")
is_success_translated = self.whale_translator.translate_image(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.update_detail_progress(i,total_images)
@ -866,8 +873,7 @@ class TranslationApp(QWidget):
# 옵션 최대선택갯수
max_option_count = 20
option_image_trans = False
await self.optionHandler.process_options(product_name, max_option_count, self.toggle_states)
self.current_options_info = await self.optionHandler.process_options(product_name, max_option_count, self.toggle_states)
# 수정 후 저장
# await self.optionHandler.save_option()

View File

@ -25,6 +25,8 @@ class LocatorManager:
'option_count_text_locator': self.config.get('PriceLocators', 'option_count_text_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("'"),
'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 섹션
@ -83,6 +85,7 @@ class LocatorManager:
'file_input_locator': self.config.get('OptionLocators', 'file_input_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("'"),
'one_to_nine_button_locator': self.config.get('OptionLocators', 'one_to_nine_button_locator').strip("'"),
}
# TitleLocators 섹션

104
option.py
View File

@ -13,6 +13,8 @@ class OptionHandler:
self.debug_flag = debug_flag
self.vertexAItranslator = vertexAI
self.whale_translator = whale_translator
self.is_percenty_success = False
self.is_vertext_success = False
self.init_option_info()
# 선택자 로드
@ -34,6 +36,7 @@ class OptionHandler:
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.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):
self.page = page1
@ -41,6 +44,8 @@ class OptionHandler:
def init_option_info(self):
self.option_info = {
'is_single_option': False,
'is_completed_option': False,
'original_names': {},
'translated_names': {},
'selected_translated_options': {},
@ -54,6 +59,7 @@ class OptionHandler:
def get_selected_translated_options(self):
"""클래스 변수에 저장된 선택된 번역된 옵션들을 반환"""
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):
@ -116,10 +122,14 @@ class OptionHandler:
async def store_selected_options(self):
"""현재 페이지에서 선택된 옵션을 수집하여, 가격 낮은 순으로 정렬한 후 클래스 변수에 저장"""
try:
selected_translated_options = []
selected_translated_options = {}
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()
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)
if option_input_element:
option_name_value = (await option_input_element.get_attribute('value')).strip()
selected_translated_options.append(
# (option_name_value, self.option_info['prices'].get(option_name_value, {}).get('low_price', 0))
(option_name_value)
)
selected_translated_options[option_name_value] = self.option_info['prices'].get(option_name_value, {}).get('low_price', 0)
self.option_info['selected_translated_options'] = selected_translated_options
self.logger.debug(f"선택된 옵션 저장 완료: {selected_translated_options}")
@ -148,10 +155,13 @@ class OptionHandler:
옵션 처리 로직. 옵션을 번역하고 이미지를 업데이트함.
:param product_name: 상품명 (str 형태).
:param max_option_count: 최대 옵션 갯수 (기본값 10).
:param max_option_count: 최대 옵션 갯수 (기본값 20).
"""
try:
self.logger.debug(f"상품명: {product_name}에 대한 옵션을 처리 중...")
self.logger.debug(f"이전 상품의 옵션정보를 초기화합니다.")
self.init_option_info()
# self.logger.debug(f"포커스를 위해 클릭1회")
# self.low_order_click()
@ -168,15 +178,23 @@ class OptionHandler:
# 1. 단일 옵션인지 판단
if await self.is_single_option():
self.logger.debug("단일 옵션 상품입니다. 옵션 수정 과정을 생략합니다.")
return
self.option_info['is_single_option'] = True
return self.option_info
# 2. 전체 옵션 체크박스 상태 확인
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("옵션이 일부만 체크된 상태입니다. 옵션 수정이 완료된 상품으로 판단하여 패스합니다.")
return
self.option_info['is_completed_option'] = True
return self.option_info
# 3. 가격 낮은 순 정렬 클릭
await self.low_order_click()
@ -187,15 +205,13 @@ class OptionHandler:
self.logger.debug(f"옵션 AI번역 : {toggle_states['optionTrnas']}")
self.option_info = await self.collect_options_info()
translation_success = False # 성공/실패 플래그
try:
# Vertex AI를 통한 번역 시도
translated_options = await self.vertexAItranslator.translate_options(self.option_info['original_names'], product_name)
self.logger.debug(f"번역된 옵션 입력")
await self.apply_translated_options(translated_options, self.option_info['edit_fields'])
translation_success = True # 번역 성공
self.is_vertext_success = True # 번역 성공
except ValueError as ve:
# 안전 필터 예외 처리
@ -205,7 +221,8 @@ class OptionHandler:
await self.page.click(self.ai_option_btn_selector)
self.logger.debug("번역을 위한 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:
# # 할당량 초과 예외 처리
@ -223,10 +240,11 @@ class OptionHandler:
await self.page.click(self.ai_option_btn_selector)
self.logger.debug("번역을 위한 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:
# 옵션 처리 중 오류 발생 시 전체 로그 출력
@ -237,9 +255,10 @@ class OptionHandler:
self.logger.debug(f"옵션 필터링 및 조정 : {toggle_states['optionAutoSelect']}")
await self.filter_and_adjust_options(max_option_count)
# 6. 선택된 옵션 재수집
self.logger.debug(f"옵션 필터링 및 조정")
await self.store_selected_options() # 페이지에서 실제 선택된 옵션을 수집하여 저장
# 6. 선택된 옵션정보 재수집
if self.is_percenty_success:
self.logger.debug(f"퍼센티 옵션번역으로 인해 선택된 옵션정보 재수집")
await self.store_selected_options() # 페이지에서 실제 선택된 옵션을 수집하여 저장
# 7. 옵션 이미지 업데이트 (옵션 이미지가 있는 경우)
if toggle_states['optionIMGTrans']:
@ -248,39 +267,26 @@ class OptionHandler:
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)
# # Vertex AI를 통해 옵션명을 번역
# translated_options = await self.vertexAItranslator.translate_options(self.option_info['original_names'], product_name)
# self.logger.debug(f"번역된 옵션: {translated_options}")
# 8. A-Z or 1-99 button 클릭
# # 5. 번역된 옵션명 편집칸에 입력
# self.logger.debug("번역된 옵션명을 입력합니다.")
# await self.apply_translated_options(translated_options, self.option_info['edit_fields'])
# # 6. 옵션 이미지 업데이트 (옵션 이미지가 있는 경우)
# self.logger.debug("옵션 이미지 업데이트 (옵션 이미지가 있는 경우)")
# for index, option_image_url in enumerate(self.option_info.get('option_images', []), start=1):
# 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)
# # 7. 옵션 선택 및 제한 처리
# await self.adjust_options(self.option_info['checkboxes'], max_option_count)
# # 8. 정리된 옵션을 다시한번 더 가격 낮은 순으로 정렬 클릭
# await self.low_order_click()
what_prefix_button = '1-99'
# 9. A-Z 버튼 클릭
self.logger.debug("A-Z 버튼을 클릭합니다.")
self.AtoZ_button_click()
if what_prefix_button == 'A-Z':
await self.AtoZ_button_click()
elif what_prefix_button == '1-99':
# # 9. A-Z 버튼 클릭
await self.one_to_nine_button_click()
# 9. 저장 버튼 클릭
self.logger.debug("저장 버튼을 클릭합니다.")
await self.page.click('button:has-text("저장하기")')
self.logger.debug("옵션 처리 완료.")
return self.option_info
except Exception as e:
self.logger.debug(f"옵션 처리 중 오류 발생: {e}", exc_info=True)
return
return None
async def is_single_option(self):
"""단일 상품 상태 여부를 확인하는 메서드"""
@ -347,7 +353,6 @@ class OptionHandler:
async def collect_options_info(self):
"""옵션 정보를 수집 (이미지, 옵션명, 편집 필드, 가격, 체크박스 정보 포함)"""
self.init_option_info()
try:
# 총 옵션 갯수 수집
total_options_element = await self.page.query_selector(self.total_options_selector)
@ -375,6 +380,12 @@ class OptionHandler:
elements = await asyncio.gather(*tasks)
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:
original_name = await original_name_element.inner_text()
self.option_info['original_names'][f'origin_option_{i}'] = original_name
@ -383,6 +394,7 @@ class OptionHandler:
# 체크박스 상태 수집 (체크 여부는 클래스 이름으로 판단)
checkbox_state = None
if checkbox_element:
checkbox_classes = await checkbox_element.get_attribute('class')
if 'ant-checkbox-checked' in checkbox_classes:
@ -633,6 +645,10 @@ class OptionHandler:
self.logger.debug("A-Z 버튼을 클릭합니다.")
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):
self.logger.debug("가격 낮은 순 정렬을 클릭합니다.")
await self.page.click(self.low_order_button_locator)
@ -696,7 +712,7 @@ class OptionHandler:
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)
self.logger.debug(f"{index}번째 옵션에 번역된 이미지가 추가되었습니다.")
@ -720,7 +736,7 @@ class OptionHandler:
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)
self.logger.debug(f"{index}번째 옵션에 원본 이미지가 업로드되었습니다.")
else:

View File

@ -7,9 +7,10 @@ import math , re, time
from playwright.async_api import TimeoutError
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.browser_controller = browser_controller
self.optionHandler = optionHandler
self.page = self.browser_controller.page
self.logger = logger
self.debug_flag = debug_flag
@ -27,10 +28,13 @@ class PriceHandler:
self.first_delv_fee = 0
self.exchange_fee = 0
self.sold_price = 0
self.initail_margin_cost = 0
# Locator들을 미리 로드하여 초기화 - locator_template:동적 로딩, 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.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.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')
@ -86,7 +90,10 @@ class PriceHandler:
self.logger.debug(f"더하기마진값{initial_plusmargin}을 팔린가격{sold_price}으로 간주")
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:
self.logger.error("상품 옵션 정보를 수집하지 못했습니다.", exc_info=True)
return
@ -149,7 +156,7 @@ class PriceHandler:
"""
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:
return_fee = self.round_to_UP(return_fee)
await return_fee_element.fill(str(return_fee)) # 기존 내용을 지우고 새 값을 입력
@ -158,7 +165,7 @@ class PriceHandler:
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:
first_delv_fee = self.round_to_UP(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)
# 교환비 수정
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:
exchange_fee = self.round_to_UP(exchange_fee)
await exchange_fee_element.fill(str(exchange_fee)) # 기존 내용을 지우고 새 값을 입력
@ -224,12 +231,12 @@ class PriceHandler:
shipping_cost = self.round_to_UP(shipping_cost)
# 더하기 마진 입력 (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))
self.logger.debug(f"더하기 마진 입력 완료: {margin}")
# 해외 배송비 입력 (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))
self.logger.debug(f"해외 배송비 입력 완료: {shipping_cost}")
@ -331,6 +338,10 @@ class PriceHandler:
- adjusted_margin (int): 적정 판매가에 맞게 조정된 더하기 마진.
"""
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))
self.logger.debug(f"총 옵션 기준 판매가 평균: {total_option_price}")
@ -503,11 +514,11 @@ class PriceHandler:
initail_shipping_cost = self.round_to_UP(initail_shipping_cost)
self.logger.debug(f"계산원가 기준 해외배송비: {initail_shipping_cost}")
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.logger.debug(f"계산원가 기준 더하기마진: {initail_margin_cost}")
self.initail_margin_cost = self.calculate_shipping_cost_with_extended_thresholds(10000, initial_cost_price)
self.initail_margin_cost = self.round_to_UP(self.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}")
return result
@ -528,7 +539,7 @@ class PriceHandler:
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()
@ -551,7 +562,7 @@ class PriceHandler:
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:
# 옵션 개수 가져오기 (두 가지 방법 중 하나 사용)
total_options = await self.get_option_count_from_text()
if is_single:
total_options = 1
else:
total_options = await self.get_option_count_from_text()
self.product_costs = [] # 모든 옵션의 상품원가를 저장할 리스트
self.option_data = [] # 각 옵션의 상품원가와 기준판매가를 저장할 리스트
self.logger.debug(f"collect_product_costs_and_prices 단일옵션 : {is_single}")
# 옵션 개수만큼 순회하면서 값을 수집
for i in range(1, total_options + 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)
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)
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)
standard_selling_price_element = await self.page.wait_for_selector(standard_selling_price_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, timeout=5000)
# cost_value = int(float(await product_cost_element.input_value().replace(",", "")))
cost_value_str = await product_cost_element.input_value()
@ -634,12 +655,12 @@ class PriceHandler:
"""
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 = int(margin_value.replace(",", "")) if margin_value else 0
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_cost = int(shipping_value.replace(",", "")) if shipping_value else 0
self.logger.debug(f"해외 배송비 값: {shipping_cost}")

View File

@ -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}",
"detail_page_prompt_template": "상세 페이지 번역 요청: {detail_page}",
"title_prompt_template": "제목 번역 요청: {title}",

View File

@ -765,16 +765,17 @@ class CMBSettingsDialog(QDialog):
query = '''
SELECT id, category1, category2, category3, category4, crmobi_stage
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:
# 검색어가 없는 경우 전체 로드
query = '''
SELECT id, category1, category2, category3, category4, crmobi_stage
FROM categories
'''
args = []
args = {}
# 검색 결과를 트리에 표시
self.category_tree.clear()
@ -812,53 +813,53 @@ class CMBSettingsDialog(QDialog):
self.logger.error(f"카테고리 검색 중 오류: {e}", exc_info=True)
def get_crmobi_stage_by_keyword(self, category):
"""
주어진 카테고리에 해당하는 CMB 단계가 설정되어 있는지 확인하고,
설정된 경우 해당 단계의 범위를 반환합니다.
# def get_crmobi_stage_by_keyword(self, category):
# """
# 주어진 카테고리에 해당하는 CMB 단계가 설정되어 있는지 확인하고,
# 설정된 경우 해당 단계의 범위를 반환합니다.
Parameters:
category (str): 카테고리 이름 (: '가구/인테리어')
# Parameters:
# category (str): 카테고리 이름 (예: '가구/인테리어')
Returns:
tuple: (min_amount, unit_amount, extra_cost) 값이 있는 경우
None: 설정된 CMB 단계가 없는 경우
"""
try:
# DB에서 카테고리에 대한 CMB 단계 정보를 가져옴
query = '''
SELECT crmobi_stage FROM categories
WHERE category1 = ? OR category2 = ? OR category3 = ? OR category4 = ?
'''
# Returns:
# tuple: (min_amount, unit_amount, extra_cost) 값이 있는 경우
# None: 설정된 CMB 단계가 없는 경우
# """
# try:
# # DB에서 카테고리에 대한 CMB 단계 정보를 가져옴
# query = '''
# SELECT crmobi_stage FROM categories
# WHERE category1 = ? OR category2 = ? OR category3 = ? OR category4 = ?
# '''
self.logger.debug(f"category : {category}")
args = [category] * 4
# self.db_manager.execute(query, args)
result = self.db_manager.fetchone(query, args)
# self.logger.debug(f"category : {category}")
# args = [category] * 4
# # self.db_manager.execute(query, args)
# result = self.db_manager.fetchone(query, args)
if result and result[0] > 0: # CMB 단계가 설정되어 있을 때
crmobi_stage = result[0]
# if result and result[0] > 0: # CMB 단계가 설정되어 있을 때
# crmobi_stage = result[0]
# 설정된 CMB 단계에 해당하는 범위 가져오기
stage_query = '''
SELECT threshold, increment_unit, extra_cost
FROM crmobi_stages
WHERE stage = ?
'''
# self.db_manager.execute(stage_query, (crmobi_stage,))
stage_result = self.db_manager.fetchone(stage_query, (crmobi_stage,))
self.logger.debug(f"stage_result : {stage_result}")
# # 설정된 CMB 단계에 해당하는 범위 가져오기
# stage_query = '''
# SELECT threshold, increment_unit, extra_cost
# FROM crmobi_stages
# WHERE stage = ?
# '''
# # self.db_manager.execute(stage_query, (crmobi_stage,))
# stage_result = self.db_manager.fetchone(stage_query, (crmobi_stage,))
# self.logger.debug(f"stage_result : {stage_result}")
if stage_result:
min_amount, unit_amount, extra_cost = stage_result
return (min_amount, unit_amount, extra_cost)
# if stage_result:
# min_amount, unit_amount, extra_cost = stage_result
# return (min_amount, unit_amount, extra_cost)
# CMB 단계가 설정되지 않은 경우
return None
# # CMB 단계가 설정되지 않은 경우
# return None
except Exception as e:
QMessageBox.critical(self, "DB 오류", f"DB 조회 중 오류가 발생했습니다: {e}")
return None
# except Exception as e:
# QMessageBox.critical(self, "DB 오류", f"DB 조회 중 오류가 발생했습니다: {e}")
# return None
def get_crmobi_stage(self, category):

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

BIN
src/img/page_loading.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 464 B

BIN
src/img/translating.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

BIN
test/fail_translated1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

BIN
test/fail_translated2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

41
test/html_test.py Normal file

File diff suppressed because one or more lines are too long

297
test/img_test.py Normal file
View File

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

BIN
test/page_loading.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 464 B

BIN
test/page_loading_2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 327 B

BIN
test/translating.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

View File

@ -210,6 +210,13 @@ class TitleHandler:
if await category_text_certified_element.count():
category_text = await category_text_certified_element.inner_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:
self.logger.debug(f"카테고리 text = {category_text}")

BIN
userDB.db

Binary file not shown.

View File

@ -1,26 +1,41 @@
import pyautogui
import ctypes
import os
import time
import win32gui, win32con, win32process
from pyvda import VirtualDesktop, get_virtual_desktops
import subprocess
import asyncio
import pyscreeze
import KO_EN
import pyperclip # 클립보드 데이터를 확인하기 위한 라이브러리
from PIL import ImageGrab
import re
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.logger = logger
self.vd_mode = vd_mode
self.newtab = "about:newtab"
self.whale_pid = None
self.whale_rect = None
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.failure_count = 0 # 실패 횟수
self.max_failures = max_failures # 최대 실패 횟수
if isSecret:
self.whale_window_name = "새 시크릿 탭 - Whale"
@ -59,9 +74,12 @@ class WhaleTranslator:
return
# 창 크기 조절 및 포커스 이동
win32gui.ShowWindow(self.whale_hwnd, win32con.SW_NORMAL)
win32gui.SetWindowPos(self.whale_hwnd, None, 0, 0, 1920, 1080, win32con.SWP_NOZORDER)
self.logger.debug("Whale 창 크기 조절 완료")
# win32gui.ShowWindow(self.whale_hwnd, win32con.SW_NORMAL)
# win32gui.SetWindowPos(self.whale_hwnd, None, 0, 0, 1920, 1080, win32con.SWP_NOZORDER)
# self.logger.debug("Whale 창 크기 조절 완료")
self.set_window_position(self.whale_hwnd, 1, 1, 1280, 720) # 위치 (1, 1), 크기 (1280x720)
self.update_whale_rect()
# 주소창으로 이동 후 URL 입력
pyautogui.hotkey('ctrl', 'l')
@ -80,27 +98,27 @@ class WhaleTranslator:
self.failure_count = 0
self.logger.debug("실패 횟수가 초기화되었습니다.")
def handle_translation_failure(self):
"""번역 실패 시 처리"""
self.failure_count += 1
self.logger.error(f"번역 실패! 실패 횟수: {self.failure_count}/{self.max_failures}")
# def handle_translation_failure(self):
# """번역 실패 시 처리"""
# self.failure_count += 1
# self.logger.error(f"번역 실패! 실패 횟수: {self.failure_count}/{self.max_failures}")
if self.failure_count >= self.max_failures:
self.logger.error("최대 실패 횟수에 도달했습니다. 웨일 브라우저를 재시작합니다.")
self.close_whale_window_if_exists()
time.sleep(2) # 재시작 전에 짧은 대기
asyncio.run(self.start_whale_browser()) # 브라우저 재시작
self.reset_failures() # 실패 횟수 초기화
# if self.failure_count >= self.max_failures:
# self.logger.error("최대 실패 횟수에 도달했습니다. 웨일 브라우저를 재시작합니다.")
# self.close_whale_window_if_exists()
# time.sleep(2) # 재시작 전에 짧은 대기
# asyncio.run(self.start_whale_browser()) # 브라우저 재시작
# self.reset_failures() # 실패 횟수 초기화
def is_image_in_clipboard_with_text(self):
"""클립보드에 이미지 데이터 또는 base64로 인코딩된 이미지 데이터가 있는지 확인"""
clipboard_content = pyperclip.paste()
if clipboard_content.startswith("data:image") or isinstance(clipboard_content, bytes):
self.logger.debug("클립보드에 이미지 데이터가 확인되었습니다.")
return True
else:
self.logger.debug("클립보드에 이미지 데이터가 없습니다.")
return False
# def is_image_in_clipboard_with_text(self):
# """클립보드에 이미지 데이터 또는 base64로 인코딩된 이미지 데이터가 있는지 확인"""
# clipboard_content = pyperclip.paste()
# if clipboard_content.startswith("data:image") or isinstance(clipboard_content, bytes):
# self.logger.debug("클립보드에 이미지 데이터가 확인되었습니다.")
# return True
# else:
# self.logger.debug("클립보드에 이미지 데이터가 없습니다.")
# return False
def is_image_in_clipboard(self):
"""클립보드에 이미지 데이터가 있는지 확인"""
@ -118,10 +136,29 @@ class WhaleTranslator:
return False
def find_whale_window(self):
"""웨일 창 핸들을 찾는 메서드"""
if not self.whale_hwnd:
self.whale_hwnd = self.find_window_by_title(self.whale_window_name)
return self.whale_hwnd
"""웨일 창을 제목을 기준으로 찾는 메서드"""
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 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 enum_windows_callback(hwnd, result):
@ -225,47 +262,88 @@ class WhaleTranslator:
if not self.whale_hwnd:
# 웨일 창을 찾지 못했을 경우 사용자에게 입력 받기
self.logger.debug("웨일 창을 찾지 못했습니다. 계속하려면 'y'를 입력하세요.")
self.logger.debug("웨일 창을 찾지 못했습니다. 새로운 웨일창 호출.")
self.create_and_update_whale_window()
if self.whale_hwnd:
try:
self.logger.debug(f"웨일 창을 찾았습니다.{self.whale_hwnd}")
win32gui.ShowWindow(self.whale_hwnd, win32con.SW_RESTORE) # 웨일 창 활성화
win32gui.SetForegroundWindow(self.whale_hwnd)
pyautogui.moveTo(960,580) # 마우스 센터로 이동
# pyautogui.hotkey('ctrl', 'l') # 웨일 브라우저의 주소창으로 이동
self.enter_url(url)
self.logger.debug(f"이미지 URL 주소 {url} 입력")
self.enter_url_for_clipboard(url)
pyautogui.press('enter')
# await asyncio.sleep(1) # 페이지 로딩 대기
time.sleep(1)
time.sleep(2) # 페이지 로딩 대기
pyautogui.rightClick()
# await asyncio.sleep(0.2) # 페이지 로딩 대기
time.sleep(1)
# 현재 웨일 창의 해상도를 확인하여 기준 이하일 경우 패스
min_width = 200
min_height = 150
pyautogui.press('r') # 번역 클릭
# await asyncio.sleep(7) # 페이지 로딩 대기
time.sleep(5)
if not self.check_image_size(url, min_width=min_width,min_height=min_height):
self.logger.info("해상도가 기준보다 낮아 작업을 패스합니다.")
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) # 클립보드 업데이트 대기
self.logger.debug("클립보드에 이미지 데이터가 존재하는지 확인 중.....")
if self.is_image_in_clipboard(): # 클립보드에 이미지 데이터가 있으면 성공
self.translation_success_flag = True
self.logger.debug(f'번역 성공: {url}')
self.reset_failures() # 번역 성공 시 연속 실패 횟수 초기화
self.logger.info(f'번역 성공: {url}')
# self.reset_failures() # 번역 성공 시 연속 실패 횟수 초기화
else:
self.logger.error(f'번역 실패: 클립보드에 이미지 데이터가 없음')
self.handle_translation_failure()
# self.handle_translation_failure()
self.enter_url(self.newtab)
self.logger.debug(f'번역 완료: {url}')
self.logger.info(f'번역 프로세스 완료. 웨일 기본페이지로 돌아감')
# self.enter_url(self.newtab)
pyautogui.hotkey('alt', 'left') # 클립보드 이미지 붙여넣기
# self.logger.debug(f'번역 완료: {url}')
if self.vd_mode:
self.return_to_virtual_desktop_1()
@ -275,12 +353,29 @@ class WhaleTranslator:
pass # 클립보드의 이미지를 path의 파일로 저장하고 저장경로를 리턴하는 메서드
# path에는 현재 폴더의 tmp_img폴더에 상품명-옵션명 형태로 제공됨
return True
except Exception as e:
self.logger.error(f"번역 중 오류 발생: {e}")
self.handle_translation_failure()
self.logger.error(f"번역 중 오류 발생: {e}", exc_info=True)
return False
# self.handle_translation_failure()
else:
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):
"""
@ -427,3 +522,263 @@ class WhaleTranslator:
except Exception as e:
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 # 작업 성공