import pyautogui import ctypes import time import win32gui, win32con, win32process from pyvda import VirtualDesktop, get_virtual_desktops import subprocess import asyncio import KO_EN import pyperclip # 클립보드 데이터를 확인하기 위한 라이브러리 from PIL import ImageGrab class WhaleTranslator: def __init__(self, app, logger, secret_mode=True, vd_mode=False, max_failures=5): self.app = app self.logger = logger self.vd_mode = vd_mode self.newtab = "about:newtab" self.whale_pid = None isSecret = secret_mode self.translation_success_flag = False # 번역 성공 플래그 self.failure_count = 0 # 실패 횟수 self.max_failures = max_failures # 최대 실패 횟수 if isSecret: self.whale_window_name = "새 시크릿 탭 - Whale" else: self.whale_window_name = "새 탭 - Whale" self.whale_hwnd = None def start_whale_browser(self): """비동기 브라우저 시작 및 가상 데스크탑 처리""" if self.vd_mode: self.ensure_virtual_desktop_2_exists() # 가상 데스크탑 2 생성 # Whale 브라우저 실행 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}") # 창을 찾기 전 대기 (최대 10초 동안 반복 시도) wait_time = 0 self.whale_hwnd = None while self.whale_hwnd is None and wait_time < 10: self.whale_hwnd = win32gui.FindWindow(None, self.whale_window_name) if self.whale_hwnd: self.logger.debug(f"Whale 창을 찾았습니다: {self.whale_hwnd}") break time.sleep(1) wait_time += 1 self.logger.debug(f"Whale 창을 찾지 못했습니다. 재시도 중... ({wait_time}초 경과)") # 창을 찾지 못한 경우 처리 if not self.whale_hwnd: self.logger.debug("Whale 창을 찾을 수 없습니다.") 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 창 크기 조절 완료") # 주소창으로 이동 후 URL 입력 pyautogui.hotkey('ctrl', 'l') time.sleep(0.4) pyautogui.typewrite('https://daum.net') self.change_lang() self.enter_url("about:newtab", change=True) self.logger.debug("URL 입력 완료") # 가상 데스크탑 복귀 처리 if self.vd_mode: self.return_to_virtual_desktop_1() # 가상 데스크탑 1로 복귀 def reset_failures(self): """실패 횟수를 초기화""" 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}") 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(self): """클립보드에 이미지 데이터가 있는지 확인""" try: # 클립보드에서 이미지를 가져오고 None이 아니면 이미지가 있는 것으로 간주 image = ImageGrab.grabclipboard() if isinstance(image, bytes) or image is not None: self.logger.debug("클립보드에 이미지 데이터가 확인되었습니다.") return True else: self.logger.debug("클립보드에 이미지 데이터가 없습니다.") return False except Exception as e: self.logger.error(f"클립보드에서 이미지 확인 중 오류 발생: {e}", exc_info=True) 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 find_window_by_title(self, window_name): def enum_windows_callback(hwnd, result): if win32gui.IsWindowVisible(hwnd) and window_name in win32gui.GetWindowText(hwnd): result.append(hwnd) result = [] win32gui.EnumWindows(enum_windows_callback, result) return result[0] if result else None def find_whale_window_pid(self): """프로세스 ID를 기반으로 웨일 창 핸들을 찾는 메서드""" if not self.whale_hwnd: self.whale_hwnd = self.find_window_by_pid(self.whale_pid) return self.whale_hwnd def find_window_by_pid(self, pid): """PID와 창 제목을 통해 유일한 웨일 창을 찾는 메서드""" def enum_windows_callback(hwnd, pid_list): tid, found_pid = win32process.GetWindowThreadProcessId(hwnd) window_title = win32gui.GetWindowText(hwnd) # 웨일의 창 제목이 일치하고 PID도 일치하는 창을 찾음 if found_pid == pid and self.whale_window_name in window_title: pid_list.append(hwnd) hwnd_list = [] win32gui.EnumWindows(enum_windows_callback, hwnd_list) if hwnd_list: self.logger.debug(f"웨일 창을 찾았습니다: {hwnd_list[0]}") return hwnd_list[0] else: self.logger.debug(f"PID {pid}에 해당하는 웨일 창을 찾지 못했습니다.") return None def find_window_by_pid_ori(self, pid): """프로세스 ID를 기반으로 창 핸들을 찾는 메서드""" def enum_windows_callback(hwnd, pid_list): tid, found_pid = win32process.GetWindowThreadProcessId(hwnd) if found_pid == pid: pid_list.append(hwnd) hwnd_list = [] win32gui.EnumWindows(enum_windows_callback, hwnd_list) return hwnd_list[0] if hwnd_list else None def ensure_virtual_desktop_2_exists(self): """가상 데스크톱 2가 존재하는지 확인하고, 없으면 생성""" try: # 현재 활성화된 가상 데스크톱 수 확인 desktops = get_virtual_desktops() number_of_desktops = len(desktops) # 가상 데스크톱 2가 존재하지 않으면 생성 if number_of_desktops < 2: pyautogui.hotkey('win', 'ctrl', 'd') # 새 가상데스크탑 생성 self.logger.debug("가상 데스크톱 2가 생성되었습니다.") time.sleep(1) else: self.switch_to_virtual_desktop_2() self.close_whale_window_if_exists() self.logger.debug("가상 데스크톱 2가 이미 존재합니다.") except Exception as e: self.logger.debug(f"가상 데스크톱 확인/생성 중 오류 발생: {e}", exc_info=True) def switch_to_whale(self): """웨일로 포커스 전환""" if self.whale_hwnd: win32gui.ShowWindow(self.whale_hwnd, win32con.SW_RESTORE) win32gui.SetForegroundWindow(self.whale_hwnd) self.logger.debug('크롬 창으로 포커스 이동.') else: self.logger.debug('크롬 창을 찾을 수 없습니다.') def switch_to_virtual_desktop_2(self): """가상 데스크톱 2로 전환""" try: VirtualDesktop(2).go() self.logger.debug("가상 데스크톱 2로 전환되었습니다.") time.sleep(0.3) except Exception as e: self.logger.debug(f"가상 데스크톱 전환 중 오류 발생: {e}", exc_info=True) def return_to_virtual_desktop_1(self): """가상 데스크톱 1로 복귀""" try: VirtualDesktop(1).go() self.logger.debug("가상 데스크톱 1로 전환되었습니다.") time.sleep(0.3) except Exception as e: self.logger.debug(f"가상 데스크톱 전환 중 오류 발생: {e}", exc_info=True) def translate_image(self, url, path=None): if self.vd_mode: self.switch_to_virtual_desktop_2() if not self.whale_hwnd: # 웨일 창을 찾지 못했을 경우 사용자에게 입력 받기 self.logger.debug("웨일 창을 찾지 못했습니다. 계속하려면 'y'를 입력하세요.") self.create_and_update_whale_window() if self.whale_hwnd: try: 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) pyautogui.press('enter') # await asyncio.sleep(1) # 페이지 로딩 대기 time.sleep(1) pyautogui.rightClick() # await asyncio.sleep(0.2) # 페이지 로딩 대기 time.sleep(1) pyautogui.press('r') # 번역 클릭 # await asyncio.sleep(7) # 페이지 로딩 대기 time.sleep(5) pyautogui.rightClick() # await asyncio.sleep(0.2) # 페이지 로딩 대기 time.sleep(1) pyautogui.press('c') # 번역된 이미지 클립보드에 복사 time.sleep(1) # 클립보드 업데이트 대기 if self.is_image_in_clipboard(): # 클립보드에 이미지 데이터가 있으면 성공 self.translation_success_flag = True self.logger.debug(f'번역 성공: {url}') self.reset_failures() # 번역 성공 시 연속 실패 횟수 초기화 else: self.logger.error(f'번역 실패: 클립보드에 이미지 데이터가 없음') self.handle_translation_failure() self.enter_url(self.newtab) self.logger.debug(f'번역 완료: {url}') if self.vd_mode: self.return_to_virtual_desktop_1() # 경로를 인자로 받을경우 해당경로에 파일 저장 if path: pass # 클립보드의 이미지를 path의 파일로 저장하고 저장경로를 리턴하는 메서드 # path에는 현재 폴더의 tmp_img폴더에 상품명-옵션명 형태로 제공됨 except Exception as e: self.logger.error(f"번역 중 오류 발생: {e}") self.handle_translation_failure() else: self.logger.debug('웨일 창을 찾을 수 없습니다.') self.handle_translation_failure() def create_and_update_whale_window(self): """ 웨일 창을 찾지 못했을 경우 새 웨일 창을 생성하고 핸들을 업데이트하는 메서드. """ self.logger.debug("웨일 창을 찾지 못해 새 웨일 창을 생성합니다.") # 웨일 브라우저 실행 whale_path = r"C:\\Program Files\\Naver\\Naver Whale\\Application\\whale.exe" process = subprocess.Popen([whale_path, '--incognito']) self.whale_pid = process.pid self.logger.debug(f"새 웨일 브라우저 실행, PID: {self.whale_pid}") # await asyncio.sleep(2) # 브라우저가 실행될 때까지 대기 time.sleep(2) # 창 핸들 업데이트 self.whale_hwnd = self.find_whale_window() if self.whale_hwnd: self.logger.debug(f"새로 생성된 웨일 창 핸들: {self.whale_hwnd}") win32gui.ShowWindow(self.whale_hwnd, win32con.SW_NORMAL) win32gui.SetWindowPos(self.whale_hwnd, None, 0, 0, 1920, 1080, win32con.SWP_NOZORDER) else: self.logger.debug("새로 생성된 웨일 창을 찾지 못했습니다.") # def switch_language(self): # # Shift 키 누름 # pyautogui.keyDown('shift') # # 0.1초 대기 후 Alt 키 누름 # time.sleep(1) # pyautogui.keyDown('alt') # # 0.5초 대기 # time.sleep(1) # # Alt 키 해제 # pyautogui.keyUp('alt') # # Shift 키 해제 # pyautogui.keyUp('shift') # def set_input_language(self, lang='EN'): # # 윈도우에서 입력 언어를 변경하는 코드 # # lang='EN' 또는 lang='KR'을 설정 가능 # user32 = ctypes.WinDLL('user32', use_last_error=True) # if lang == 'EN': # # 영어로 변경 # user32.LoadKeyboardLayoutW('00000409', 1) # elif lang == 'KR': # # 한글로 변경 # user32.LoadKeyboardLayoutW('00000412', 1) def change_lang(self): # 언어 전환이 완료되었는지 확인하는 함수 def wait_for_language_switch(): # 최대 3초까지 언어 전환 확인 시도 (0.1초 간격) for _ in range(30): if KO_EN.get_hanguel_state() == 0: # 영어로 전환된 상태가 0 return True time.sleep(0.1) # await asyncio.sleep(0.1) return False # 주소창에 URL 입력하기 전에 영어로 변경 KO_EN.set_keyboard_language('eng') # 언어 전환 완료 대기 is_switched = wait_for_language_switch() if not is_switched: self.logger.debug("영어로 전환하는데 실패했습니다.") return else: self.logger.debug("전환 성공") def enter_url(self, url, change=False): # 언어 전환이 완료되면 주소창으로 이동 후 URL 입력 pyautogui.hotkey('ctrl', 'l') # 주소창으로 이동 time.sleep(0.5) # 주소창 이동 후 잠시 대기 pyautogui.typewrite(url) # URL 입력 pyautogui.press('enter') # Enter 키 입력 time.sleep(1) # 페이지 로딩 대기 def enter_url_for_clipboard(self, url, change=False): # URL을 클립보드에 복사 pyperclip.copy(url) # 언어 전환이 완료되면 주소창으로 이동 후 URL 입력 pyautogui.hotkey('ctrl', 'l') # 주소창으로 이동 time.sleep(0.5) # 주소창 이동 후 잠시 대기 pyautogui.hotkey('ctrl', 'v') # 주소 입력 pyautogui.press('enter') # Enter 키 입력 time.sleep(1) # 페이지 로딩 대기 def close_whale_window_if_exists(self): """웨일 브라우저 창을 프로세스 ID(pid)로 찾아 종료""" try: if not self.whale_pid: self.logger.debug("웨일 프로세스 ID가 설정되지 않았습니다.") return def enum_windows_callback(hwnd, result): _, found_pid = win32process.GetWindowThreadProcessId(hwnd) if found_pid == self.whale_pid and win32gui.IsWindowVisible(hwnd): result.append(hwnd) result = [] win32gui.EnumWindows(enum_windows_callback, result) if result: hwnd = result[0] self.logger.debug(f"웨일 창을 찾았습니다. 핸들: {hwnd}. 종료 중...") win32gui.PostMessage(hwnd, win32con.WM_CLOSE, 0, 0) # 창을 종료하는 메시지 전송 time.sleep(1) self.logger.debug(f"웨일 창이 성공적으로 종료되었습니다.") else: self.logger.debug("웨일 창을 찾을 수 없습니다.") except Exception as e: self.logger.error(f"웨일 창을 종료하는 중 오류 발생: {e}", exc_info=True) def close_all_virtual_desktops(self): """모든 가상 데스크톱을 종료""" try: desktops = get_virtual_desktops() number_of_desktops = len(desktops) # 가상 데스크톱이 1개 이상일 때만 종료 while number_of_desktops > 1: self.close_whale_window_if_exists() # 웨일 브라우저 창이 있으면 종료 pyautogui.hotkey('win', 'ctrl', 'f4') # 현재 가상 데스크톱 닫기 time.sleep(1) # 각 데스크톱 닫기 사이에 짧은 대기시간 추가 desktops = get_virtual_desktops() number_of_desktops = len(desktops) self.logger.debug(f"남은 가상 데스크톱 수: {number_of_desktops}") self.logger.debug("모든 가상 데스크톱이 종료되었습니다.") except Exception as e: self.logger.debug(f"가상 데스크톱 종료 중 오류 발생: {e}", exc_info=True)