This commit is contained in:
parent
5f0e48ef56
commit
d52b0b0f37
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
16982
appTranslator.log
16982
appTranslator.log
File diff suppressed because it is too large
Load Diff
|
|
@ -1,202 +0,0 @@
|
|||
import base64
|
||||
import pyperclip
|
||||
import win32clipboard
|
||||
from io import BytesIO
|
||||
from PIL import Image
|
||||
import requests
|
||||
import numpy as np
|
||||
import cv2
|
||||
import time
|
||||
import random
|
||||
|
||||
class base64TOImage:
|
||||
def __init__(self, app, logger, browser_controller):
|
||||
self.app = app
|
||||
self.logger = logger
|
||||
self.browser_controller = browser_controller # BrowserController 인스턴스를 전달받음
|
||||
|
||||
def get_clipboard_data(self):
|
||||
"""클립보드의 텍스트 데이터를 가져옵니다."""
|
||||
try:
|
||||
return pyperclip.paste() # 클립보드의 텍스트 데이터를 가져옴
|
||||
except Exception as e:
|
||||
self.logger.debug(f"클립보드 데이터를 가져오는 중 오류 발생: {e}")
|
||||
return None
|
||||
|
||||
# def set_image_to_clipboard(self, image):
|
||||
# """이미지를 클립보드에 넣는 함수 (Windows 전용)"""
|
||||
# output = BytesIO()
|
||||
# image.save(output, "BMP")
|
||||
# data = output.getvalue()[14:] # BMP 헤더 제거
|
||||
# output.close()
|
||||
|
||||
# # 클립보드에 이미지 데이터 넣기
|
||||
# win32clipboard.OpenClipboard()
|
||||
# win32clipboard.EmptyClipboard()
|
||||
# win32clipboard.SetClipboardData(win32clipboard.CF_DIB, data)
|
||||
# win32clipboard.CloseClipboard()
|
||||
|
||||
def set_image_to_clipboard(self, image):
|
||||
"""이미지를 클립보드에 넣는 함수 (Windows 전용, 5% 크롭 후)"""
|
||||
# 이미지의 크기 계산 (5% 크롭)
|
||||
width, height = image.size
|
||||
left = width * 0.05
|
||||
top = height * 0.05
|
||||
right = width * 0.95
|
||||
bottom = height * 0.95
|
||||
|
||||
# 5% 크롭 적용
|
||||
cropped_image = image.crop((left, top, right, bottom))
|
||||
|
||||
# 이미지를 BMP 형식으로 변환하여 클립보드에 넣기
|
||||
output = BytesIO()
|
||||
cropped_image.save(output, "BMP")
|
||||
data = output.getvalue()[14:] # BMP 헤더 제거
|
||||
output.close()
|
||||
|
||||
# 클립보드에 이미지 데이터 넣기
|
||||
win32clipboard.OpenClipboard()
|
||||
win32clipboard.EmptyClipboard()
|
||||
win32clipboard.SetClipboardData(win32clipboard.CF_DIB, data)
|
||||
win32clipboard.CloseClipboard()
|
||||
|
||||
self.logger.debug("5% 크롭된 이미지가 클립보드에 저장되었습니다.")
|
||||
|
||||
|
||||
def base64_to_image(self, base64_data):
|
||||
"""Base64 데이터를 이미지로 변환하는 함수"""
|
||||
if base64_data.startswith('data:image'):
|
||||
header, encoded = base64_data.split(',', 1)
|
||||
img_data = base64.b64decode(encoded)
|
||||
image = Image.open(BytesIO(img_data))
|
||||
return image
|
||||
else:
|
||||
self.logger.debug("유효하지 않은 Base64 이미지 데이터입니다.")
|
||||
return None
|
||||
|
||||
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",
|
||||
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9",
|
||||
"Accept-Language": "en-US,en;q=0.9",
|
||||
"Accept-Encoding": "gzip, deflate, br",
|
||||
"DNT": "1", # Do Not Track 요청 헤더
|
||||
"Connection": "keep-alive",
|
||||
"Upgrade-Insecure-Requests": "1",
|
||||
"Cache-Control": "max-age=0"
|
||||
}
|
||||
|
||||
retries = 0
|
||||
while retries < max_retries:
|
||||
try:
|
||||
self.logger.debug(f"이미지 URL 다운로드 중: {url}")
|
||||
response = requests.get(url, headers=headers, stream=True)
|
||||
|
||||
# 상태 코드가 200이 아니면 재시도
|
||||
if response.status_code == 200:
|
||||
# OpenCV로 이미지를 로드하여 변환
|
||||
image = np.asarray(bytearray(response.content), dtype="uint8")
|
||||
image = cv2.imdecode(image, cv2.IMREAD_COLOR)
|
||||
|
||||
# OpenCV에서 이미지를 PIL로 변환
|
||||
if image is not None:
|
||||
pil_image = Image.fromarray(cv2.cvtColor(image, cv2.COLOR_BGR2RGB))
|
||||
return pil_image
|
||||
else:
|
||||
self.logger.debug(f"이미지 파일 형식이 올바르지 않습니다. 대상 URL: {url}")
|
||||
return None
|
||||
else:
|
||||
self.logger.debug(f"이미지 로딩 실패, HTTP 상태 코드: {response.status_code}. 재시도 {retries + 1}/{max_retries}")
|
||||
retries += 1
|
||||
time.sleep(random.randint(2, 5)) # 2~5초 대기 후 재시도
|
||||
except Exception as e:
|
||||
self.logger.debug(f"이미지 로딩 중 오류 발생: {e}. 재시도 {retries + 1}/{max_retries}")
|
||||
retries += 1
|
||||
time.sleep(random.randint(2, 5)) # 예외 발생 시 대기 후 재시도
|
||||
|
||||
self.logger.debug("이미지 다운로드 최대 재시도 횟수를 초과했습니다.")
|
||||
return None
|
||||
|
||||
# def process_clipboard(self, original_url):
|
||||
# """클립보드의 내용을 처리하고, 필요한 경우 이미지 변환 또는 URL 이미지 복사"""
|
||||
# clipboard_data = self.get_clipboard_data()
|
||||
|
||||
# if clipboard_data.startswith('data:image'):
|
||||
# # 클립보드에 있는 Base64 데이터를 이미지로 변환
|
||||
# image = self.base64_to_image(clipboard_data)
|
||||
# if image:
|
||||
# self.logger.debug("Base64 이미지 변환 성공, 클립보드에 다시 저장 중...")
|
||||
# self.set_image_to_clipboard(image)
|
||||
# self.logger.debug("이미지가 클립보드에 저장되었습니다.")
|
||||
# else:
|
||||
# self.logger.debug("Base64 이미지 변환 실패.")
|
||||
|
||||
# elif clipboard_data.strip() == "html > whale-ocr":
|
||||
# # 'html > whale-ocr' 감지 시 원본 이미지 URL에서 이미지를 다운로드하여 클립보드에 복사
|
||||
# if original_url:
|
||||
# image = self.download_image_from_url(original_url)
|
||||
# if image:
|
||||
# self.logger.debug("원본 이미지 다운로드 성공, 클립보드에 다시 저장 중...")
|
||||
# self.set_image_to_clipboard(image)
|
||||
# self.logger.debug("원본 이미지가 클립보드에 저장되었습니다.")
|
||||
# else:
|
||||
# self.logger.debug("원본 이미지 다운로드 실패.")
|
||||
# else:
|
||||
# self.logger.debug("원본 이미지 URL을 찾을 수 없습니다.")
|
||||
# else:
|
||||
# self.logger.debug("클립보드에 Base64 이미지나 'html > whale-ocr' 데이터가 없습니다.")
|
||||
|
||||
def process_clipboard(self, original_url):
|
||||
"""클립보드의 내용을 처리하고, 필요한 경우 이미지 변환 또는 URL 이미지 복사"""
|
||||
clipboard_data = self.get_clipboard_data()
|
||||
|
||||
if clipboard_data.startswith('data:image'):
|
||||
# 클립보드에 있는 Base64 데이터를 이미지로 변환
|
||||
image = self.base64_to_image(clipboard_data)
|
||||
if image:
|
||||
self.logger.debug("Base64 이미지 변환 성공, 클립보드에 다시 저장 중...")
|
||||
self.set_image_to_clipboard(image)
|
||||
self.logger.debug("이미지가 클립보드에 저장되었습니다.")
|
||||
else:
|
||||
self.logger.debug("Base64 이미지 변환 실패.")
|
||||
|
||||
elif clipboard_data.strip() == "html > whale-ocr":
|
||||
# 'html > whale-ocr' 감지 시 원본 이미지 URL에서 이미지를 다운로드하여 클립보드에 복사
|
||||
if original_url:
|
||||
image = self.download_image_from_url(original_url)
|
||||
if image:
|
||||
self.logger.debug("원본 이미지 다운로드 성공, 클립보드에 다시 저장 중...")
|
||||
self.set_image_to_clipboard(image)
|
||||
self.logger.debug("원본 이미지가 클립보드에 저장되었습니다.")
|
||||
else:
|
||||
self.logger.debug("원본 이미지 다운로드 실패.")
|
||||
else:
|
||||
self.logger.debug("원본 이미지 URL을 찾을 수 없습니다.")
|
||||
|
||||
elif clipboard_data:
|
||||
# 클립보드 내용이 이미지인 경우, 이미지 크기를 검사
|
||||
image = self.base64_to_image(clipboard_data)
|
||||
if image:
|
||||
width, height = image.size
|
||||
self.logger.debug(f"클립보드에 있는 이미지 크기: {width}x{height}")
|
||||
|
||||
# 이미지가 가로나 세로 중 하나라도 200픽셀 이하인 경우 원본 이미지 다운로드
|
||||
if width <= 200 or height <= 200:
|
||||
if original_url:
|
||||
self.logger.debug(f"이미지 크기가 {width}x{height}로 200픽셀 이하입니다. 원본 이미지 다운로드 중...")
|
||||
original_image = self.download_image_from_url(original_url)
|
||||
if original_image:
|
||||
self.logger.debug("원본 이미지 다운로드 성공, 클립보드에 다시 저장 중...")
|
||||
self.set_image_to_clipboard(original_image)
|
||||
self.logger.debug("원본 이미지가 클립보드에 저장되었습니다.")
|
||||
else:
|
||||
self.logger.debug("원본 이미지 다운로드 실패.")
|
||||
else:
|
||||
self.logger.debug("원본 이미지 URL을 찾을 수 없습니다.")
|
||||
else:
|
||||
self.logger.debug("이미지 크기가 200픽셀 이상입니다. 처리할 필요가 없습니다.")
|
||||
else:
|
||||
self.logger.debug("클립보드에 있는 데이터는 이미지가 아닙니다.")
|
||||
else:
|
||||
self.logger.debug("클립보드에 Base64 이미지나 'html > whale-ocr' 데이터가 없습니다.")
|
||||
|
|
@ -4,6 +4,7 @@ import pyautogui
|
|||
import time
|
||||
import win32gui, win32con
|
||||
from bs4 import BeautifulSoup
|
||||
import pyperclip
|
||||
|
||||
class BrowserController:
|
||||
def __init__(self, app, logger):
|
||||
|
|
@ -18,9 +19,6 @@ class BrowserController:
|
|||
self.browser = None
|
||||
self.page = None
|
||||
|
||||
def get_page(self):
|
||||
return self.page
|
||||
|
||||
def start_browser(self):
|
||||
"""크롬 브라우저 실행 및 페이지 로딩"""
|
||||
self.logger.debug('크롬 브라우저 실행 중...')
|
||||
|
|
@ -41,7 +39,6 @@ class BrowserController:
|
|||
self.logger.debug('newPage 로딩 ...')
|
||||
# 사용자는 이 시점에 로그인 및 상세페이지 편집 모드로 들어감
|
||||
|
||||
|
||||
# 페이지 제목을 가져와서 창 제목으로 활용
|
||||
page_title = self.page.title()
|
||||
self.logger.debug(f'페이지 제목: {page_title}')
|
||||
|
|
@ -107,16 +104,64 @@ class BrowserController:
|
|||
else:
|
||||
self.logger.debug('크롬 창을 찾을 수 없습니다.')
|
||||
|
||||
# def switch_to_whale(self):
|
||||
# """웨일 브라우저로 포커스 전환"""
|
||||
# if not self.whale_hwnd:
|
||||
# self.whale_hwnd = self.find_window_by_title(self.whale_window_name)
|
||||
# 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 get_total_product_count(self):
|
||||
try:
|
||||
# JavaScript로 해당 요소의 텍스트를 가져옴
|
||||
element_text = self.page.evaluate('''() => {
|
||||
let element = document.querySelector('#root > div > div > div > div > main > div > div.sc-ezreuY.kYrYVh > div.sc-dChVcU.cRrUlt > div.sc-izQBue.dxiUJm > div > div:nth-child(1) > label > span:nth-child(2)');
|
||||
return element ? element.innerText : null;
|
||||
}''')
|
||||
|
||||
if element_text:
|
||||
print(f"가져온 텍스트: {element_text}") # 텍스트 확인용 로그
|
||||
# "총 xx개 상품"에서 숫자만 추출
|
||||
count = int(''.join(filter(str.isdigit, element_text)))
|
||||
return count
|
||||
else:
|
||||
print("요소를 찾을 수 없습니다.")
|
||||
return 0
|
||||
except Exception as e:
|
||||
print(f"상품 수를 가져오는 중 오류 발생: {e}")
|
||||
return 0
|
||||
|
||||
|
||||
def get_total_product_count1(self):
|
||||
"""총 상품 개수를 반환"""
|
||||
try:
|
||||
self.page.wait_for_load_state('domcontentloaded') # 페이지 로딩 완료 대기
|
||||
|
||||
total_count_css_selector = '//label/span[contains(text(),"총")]'
|
||||
total_element = self.page.wait_for_selector('//*[@id="root"]/div/div/div/div/main/div/div[2]/div[3]/div[2]/div/div[1]/label/span[2]', timeout=5000)
|
||||
total_element2 = self.page.query_selector(total_count_css_selector)
|
||||
|
||||
# JavaScript로 해당 요소의 텍스트를 가져옴
|
||||
total_count_text3 = self.page.evaluate('''() => {
|
||||
let element = document.querySelector('#root > div > div > div > div > main > div > div.sc-ezreuY.kYrYVh > div.sc-dChVcU.cRrUlt > div.sc-izQBue.dxiUJm > div > div:nth-child(1) > label > span:nth-child(2)');
|
||||
return element ? element.innerText : null;
|
||||
}''')
|
||||
|
||||
self.logger.debug(f'{total_element.inner_text()}')
|
||||
self.logger.debug(f'{total_element2.inner_text()}')
|
||||
self.logger.debug(f'{total_count_text3}')
|
||||
|
||||
total_count_text = 1
|
||||
total_count = int(re.findall(r'\d+', total_count_text)[0])
|
||||
self.logger.debug(f'총 상품수 : {total_count}개')
|
||||
return total_count
|
||||
except Exception as e:
|
||||
self.logger.debug(f"총 상품 개수 수집 중 오류 발생: {e}")
|
||||
return 0
|
||||
|
||||
def get_product_name(self, index):
|
||||
"""해당 상품의 이름을 가져옵니다. 오류 발생 시 '수집 오류 발생' 반환"""
|
||||
try:
|
||||
product_name_xpath = f"//div[{index}]/li/div/div/div[2]/div/div/div[1]/div[1]/span[2]"
|
||||
product_name_element = self.page.query_selector(product_name_xpath)
|
||||
return product_name_element.inner_text().strip()
|
||||
except Exception as e:
|
||||
self.logger.debug(f"상품명 수집 중 오류: {e}")
|
||||
return "수집 오류 발생"
|
||||
|
||||
def extract_image_urls(self):
|
||||
"""HTML에서 이미지 URL 추출 및 img 태그 삭제 후 소스 버튼 다시 클릭"""
|
||||
|
|
@ -234,9 +279,9 @@ class BrowserController:
|
|||
self.scroll_page_to_bottom()
|
||||
|
||||
|
||||
# 스크롤하여 모든 버튼을 화면에 표시 (가장 하단까지 스크롤)
|
||||
self.page.evaluate("""window.scrollTo(0, document.body.scrollHeight);""")
|
||||
self.logger.debug("페이지를 아래로 스크롤했습니다.")
|
||||
# # 스크롤하여 모든 버튼을 화면에 표시 (가장 하단까지 스크롤)
|
||||
# self.page.evaluate("""window.scrollTo(0, document.body.scrollHeight);""")
|
||||
# self.logger.debug("페이지를 아래로 스크롤했습니다.")
|
||||
|
||||
# 버튼 선택 (확실한 선택자를 사용하여 확인)
|
||||
buttons = self.page.locator('button:has-text("세부사항 수정 및 업로드")')
|
||||
|
|
@ -271,6 +316,14 @@ class BrowserController:
|
|||
except Exception as e:
|
||||
self.logger.debug(f"상세페이지 탭 클릭 중 오류: {str(e)}")
|
||||
|
||||
def click_option_tab(self):
|
||||
"""상세페이지 탭 클릭"""
|
||||
try:
|
||||
self.page.click('div.ant-tabs-tab:has-text("옵션")')
|
||||
self.logger.debug("옵션 탭 클릭 완료.")
|
||||
except Exception as e:
|
||||
self.logger.debug(f"옵션 탭 클릭 중 오류: {str(e)}")
|
||||
|
||||
def extract_image_urls(self):
|
||||
"""상세페이지에서 이미지 URL 추출"""
|
||||
try:
|
||||
|
|
@ -321,14 +374,18 @@ class BrowserController:
|
|||
except Exception as e:
|
||||
self.logger.debug(f"이미지 번역 중 오류: {str(e)}")
|
||||
|
||||
def paste_image_in_chrome(self, base64toimage, url):
|
||||
def paste_image_in_chrome(self, clipboardImageManager, url):
|
||||
"""크롬으로 포커스를 옮기고 클립보드의 이미지를 붙여넣고 엔터 입력"""
|
||||
try:
|
||||
self.switch_to_chrome() # 크롬으로 포커스 이동
|
||||
base64toimage.process_clipboard(url) # 클립보드 내용이 base64일 경우 이미지로 변환
|
||||
pyautogui.hotkey('ctrl', 'v') # 클립보드 이미지 붙여넣기
|
||||
pyautogui.press('right') # 오른쪽 입력
|
||||
self.logger.debug("이미지 붙여넣기 완료.")
|
||||
clipboardImageManager.process_clipboard(url) # 클립보드 내용을 처리
|
||||
clipboard_content = pyperclip.paste()
|
||||
if clipboard_content:
|
||||
pyautogui.hotkey('ctrl', 'v') # 클립보드 이미지 붙여넣기
|
||||
pyautogui.press('right') # 오른쪽 입력
|
||||
self.logger.debug("이미지 붙여넣기 완료.")
|
||||
else:
|
||||
self.logger.debug("클립보드가 비어있습니다.")
|
||||
except Exception as e:
|
||||
self.logger.debug(f"이미지 붙여넣기 중 오류: {str(e)}")
|
||||
|
||||
|
|
@ -344,18 +401,31 @@ class BrowserController:
|
|||
def go_to_next_page(self):
|
||||
"""다음 페이지로 이동"""
|
||||
try:
|
||||
next_button = self.page.query_selector('li.ant-pagination-next button.ant-pagination-item-link')
|
||||
# 현재 페이지가 몇 번째 페이지인지 확인 (클래스에 'ant-pagination-item-active'가 있는 요소)
|
||||
current_page = self.page.query_selector('li.ant-pagination-item.ant-pagination-item-active')
|
||||
|
||||
if next_button:
|
||||
next_button.click()
|
||||
self.page.wait_for_load_state('domcontentloaded')
|
||||
self.logger.debug("다음 페이지로 이동 완료.")
|
||||
if not current_page:
|
||||
self.logger.debug("현재 페이지 정보를 찾을 수 없습니다.")
|
||||
return False
|
||||
|
||||
# 현재 활성화된 페이지 번호를 가져옴
|
||||
current_page_number = int(current_page.get_attribute("title"))
|
||||
next_page_number = current_page_number + 1
|
||||
|
||||
# 다음 페이지 버튼을 찾음 (title 속성으로 다음 페이지를 찾음)
|
||||
next_page_button = self.page.query_selector(f'li.ant-pagination-item[title="{next_page_number}"]')
|
||||
|
||||
if next_page_button:
|
||||
next_page_button.click() # 페이지 버튼 클릭
|
||||
self.page.wait_for_load_state('domcontentloaded') # 페이지 로딩이 완료될 때까지 대기
|
||||
self.logger.debug(f"페이지 {next_page_number}로 이동 완료.")
|
||||
return True
|
||||
else:
|
||||
self.logger.debug("다음 페이지가 없습니다.")
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
self.logger.debug(f"다음 페이지로 이동 중 오류: {str(e)}")
|
||||
self.logger.debug(f"다음 페이지로 이동 중 오류 발생: {str(e)}")
|
||||
return False
|
||||
|
||||
def switch_to_chrome(self):
|
||||
|
|
|
|||
|
|
@ -0,0 +1,296 @@
|
|||
import base64
|
||||
import pyperclip
|
||||
import win32clipboard
|
||||
from io import BytesIO
|
||||
from PIL import Image
|
||||
import requests
|
||||
import numpy as np
|
||||
import cv2
|
||||
import time
|
||||
import os
|
||||
import datetime
|
||||
import random
|
||||
|
||||
class ClipboardImageManager:
|
||||
def __init__(self, app, logger, browser_controller, debug=False):
|
||||
self.app = app
|
||||
self.logger = logger
|
||||
self.browser_controller = browser_controller # BrowserController 인스턴스를 전달받음
|
||||
self.debug = debug # 디버그 플래그를 클래스 변수로 사용
|
||||
|
||||
self.debug = True
|
||||
|
||||
def get_clipboard_data(self):
|
||||
"""클립보드의 텍스트 데이터를 가져옵니다."""
|
||||
try:
|
||||
return pyperclip.paste() # 클립보드의 텍스트 데이터를 가져옴
|
||||
except Exception as e:
|
||||
self.logger.debug(f"클립보드 데이터를 가져오는 중 오류 발생: {e}")
|
||||
return None
|
||||
|
||||
# def set_image_to_clipboard(self, image):
|
||||
# """이미지를 클립보드에 넣는 함수 (Windows 전용)"""
|
||||
# output = BytesIO()
|
||||
# image.save(output, "BMP")
|
||||
# data = output.getvalue()[14:] # BMP 헤더 제거
|
||||
# output.close()
|
||||
|
||||
# # 클립보드에 이미지 데이터 넣기
|
||||
# win32clipboard.OpenClipboard()
|
||||
# win32clipboard.EmptyClipboard()
|
||||
# win32clipboard.SetClipboardData(win32clipboard.CF_DIB, data)
|
||||
# win32clipboard.CloseClipboard()
|
||||
|
||||
def set_image_to_clipboard(self, image, crop_percentage=0.05, debug=False):
|
||||
"""
|
||||
이미지를 클립보드에 넣는 함수 (Windows 전용, 크롭 후)
|
||||
|
||||
:param image: PIL 이미지 객체
|
||||
:param crop_percentage: 크롭할 비율 (0.05는 5% 크롭을 의미)
|
||||
:param debug: True일 경우 크롭 전후 다양한 비율(3%, 5%, 7%)의 이미지를 디버그 용도로 저장
|
||||
"""
|
||||
# 이미지의 크기 계산 (크롭 비율 적용)
|
||||
width, height = image.size
|
||||
left = width * crop_percentage
|
||||
top = height * crop_percentage
|
||||
right = width * (1 - crop_percentage)
|
||||
bottom = height * (1 - crop_percentage)
|
||||
|
||||
# 크롭 적용
|
||||
cropped_image = image.crop((left, top, right, bottom))
|
||||
|
||||
if debug:
|
||||
# 디버그 모드일 경우 크롭 전후 다양한 비율로 이미지 저장
|
||||
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||
original_image_path = os.path.join(os.getcwd(), f"original_image_{timestamp}.png")
|
||||
image.save(original_image_path) # 크롭 전 이미지 저장
|
||||
self.logger.debug(f"크롭 전 이미지 저장됨: {original_image_path}")
|
||||
|
||||
# 3%, 5%, 7% 크롭 이미지 저장
|
||||
crop_alternatives = [0.03, 0.05, 0.07]
|
||||
for crop in crop_alternatives:
|
||||
left_alt = width * crop
|
||||
top_alt = height * crop
|
||||
right_alt = width * (1 - crop)
|
||||
bottom_alt = height * (1 - crop)
|
||||
|
||||
cropped_alt_image = image.crop((left_alt, top_alt, right_alt, bottom_alt))
|
||||
cropped_image_path = os.path.join(os.getcwd(), f"cropped_image_{int(crop*100)}_{timestamp}.png")
|
||||
cropped_alt_image.save(cropped_image_path)
|
||||
self.logger.debug(f"{int(crop*100)}% 크롭된 이미지 저장됨: {cropped_image_path}")
|
||||
|
||||
# 크롭된 이미지를 BMP 형식으로 변환하여 클립보드에 넣기
|
||||
output = BytesIO()
|
||||
cropped_image.save(output, "BMP")
|
||||
data = output.getvalue()[14:] # BMP 헤더 제거
|
||||
output.close()
|
||||
|
||||
# 클립보드에 이미지 데이터 넣기
|
||||
win32clipboard.OpenClipboard()
|
||||
win32clipboard.EmptyClipboard()
|
||||
win32clipboard.SetClipboardData(win32clipboard.CF_DIB, data)
|
||||
win32clipboard.CloseClipboard()
|
||||
|
||||
self.logger.debug(f"{crop_percentage*100}% 크롭된 이미지가 클립보드에 저장되었습니다.")
|
||||
|
||||
def base64_to_image(self, base64_data):
|
||||
"""Base64 데이터를 이미지로 변환하는 함수"""
|
||||
if base64_data.startswith('data:image'):
|
||||
header, encoded = base64_data.split(',', 1)
|
||||
img_data = base64.b64decode(encoded)
|
||||
image = Image.open(BytesIO(img_data))
|
||||
return image
|
||||
else:
|
||||
self.logger.debug("유효하지 않은 Base64 이미지 데이터입니다.")
|
||||
return None
|
||||
|
||||
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",
|
||||
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9",
|
||||
"Accept-Language": "en-US,en;q=0.9",
|
||||
"Accept-Encoding": "gzip, deflate, br",
|
||||
"DNT": "1", # Do Not Track 요청 헤더
|
||||
"Connection": "keep-alive",
|
||||
"Upgrade-Insecure-Requests": "1",
|
||||
"Cache-Control": "max-age=0"
|
||||
}
|
||||
|
||||
retries = 0
|
||||
while retries < max_retries:
|
||||
try:
|
||||
self.logger.debug(f"이미지 URL 다운로드 중: {url}")
|
||||
response = requests.get(url, headers=headers, stream=True)
|
||||
|
||||
# 상태 코드가 200이 아니면 재시도
|
||||
if response.status_code == 200:
|
||||
# OpenCV로 이미지를 로드하여 변환
|
||||
image = np.asarray(bytearray(response.content), dtype="uint8")
|
||||
image = cv2.imdecode(image, cv2.IMREAD_COLOR)
|
||||
|
||||
# OpenCV에서 이미지를 PIL로 변환
|
||||
if image is not None:
|
||||
pil_image = Image.fromarray(cv2.cvtColor(image, cv2.COLOR_BGR2RGB))
|
||||
return pil_image
|
||||
else:
|
||||
self.logger.debug(f"이미지 파일 형식이 올바르지 않습니다. 대상 URL: {url}")
|
||||
return None
|
||||
else:
|
||||
self.logger.debug(f"이미지 로딩 실패, HTTP 상태 코드: {response.status_code}. 재시도 {retries + 1}/{max_retries}")
|
||||
retries += 1
|
||||
time.sleep(random.randint(2, 5)) # 2~5초 대기 후 재시도
|
||||
except Exception as e:
|
||||
self.logger.debug(f"이미지 로딩 중 오류 발생: {e}. 재시도 {retries + 1}/{max_retries}")
|
||||
retries += 1
|
||||
time.sleep(random.randint(2, 5)) # 예외 발생 시 대기 후 재시도
|
||||
|
||||
self.logger.debug("이미지 다운로드 최대 재시도 횟수를 초과했습니다.")
|
||||
return None
|
||||
|
||||
def process_clipboard(self, original_url):
|
||||
"""클립보드의 내용을 처리하고, 필요한 경우 이미지 변환, 크롭 또는 클립보드 비우기"""
|
||||
clipboard_data = self.get_clipboard_data()
|
||||
|
||||
# 1. 클립보드의 데이터가 Base64 이미지일 경우
|
||||
if clipboard_data.startswith('data:image'):
|
||||
self.logger.info("data:image 감지 : 이미지 데이터로 변환")
|
||||
image = self.base64_to_image(clipboard_data)
|
||||
if image:
|
||||
width, _ = image.size
|
||||
self.logger.debug(f"Base64 이미지 크기: {width}px")
|
||||
|
||||
# 가로 크기가 200픽셀 이상이면 크롭
|
||||
if width >= 200:
|
||||
self.logger.debug("이미지 가로 크기 200픽셀 이상: 크롭 진행 중...")
|
||||
cropped_image = self.crop_image(image) # 크롭 메서드 사용
|
||||
self.set_image_to_clipboard(cropped_image) # 클립보드에 저장
|
||||
else:
|
||||
self.logger.debug("이미지 가로 크기 200픽셀 이하: 클립보드 비움.")
|
||||
self.clear_clipboard()
|
||||
else:
|
||||
self.logger.debug("Base64 이미지 변환 실패.")
|
||||
|
||||
# 2. 클립보드에 이미지가 있을 경우
|
||||
elif self.is_clipboard_image():
|
||||
self.logger.info("클립보드 이미지 확인")
|
||||
image = self.get_image_from_clipboard()
|
||||
if image:
|
||||
width, _ = image.size
|
||||
self.logger.debug(f"클립보드에 있는 이미지 크기: {width}px")
|
||||
|
||||
if width >= 200:
|
||||
self.logger.debug("이미지 가로 크기 200픽셀 이상: 크롭 진행 중...")
|
||||
cropped_image = self.crop_image(image) # 크롭 메서드 사용
|
||||
self.set_image_to_clipboard(cropped_image) # 클립보드에 저장
|
||||
else:
|
||||
self.logger.debug("이미지 가로 크기 200픽셀 이하: 클립보드 비움.")
|
||||
self.clear_clipboard()
|
||||
|
||||
# 3. html > whale-ocr 처리
|
||||
elif clipboard_data.strip() == "html > whale-ocr":
|
||||
self.logger.info("html > whale-ocr 감지 : 이미지 번역 실패 확인")
|
||||
if original_url:
|
||||
image = self.download_image_from_url(original_url)
|
||||
if image:
|
||||
self.logger.debug("원본 이미지 다운로드 성공, 클립보드에 저장 중...")
|
||||
self.set_image_to_clipboard(image) # 크롭 없이 저장
|
||||
else:
|
||||
self.logger.debug("원본 이미지 다운로드 실패.")
|
||||
else:
|
||||
self.logger.debug("원본 이미지 URL을 찾을 수 없습니다.")
|
||||
|
||||
else:
|
||||
self.logger.debug("클립보드에 처리할 수 있는 데이터가 없습니다.")
|
||||
|
||||
def is_clipboard_image(self):
|
||||
"""클립보드에 이미지가 있는지 확인하는 함수"""
|
||||
return win32clipboard.IsClipboardFormatAvailable(win32clipboard.CF_DIB)
|
||||
|
||||
def get_image_from_clipboard(self):
|
||||
"""클립보드에서 이미지를 가져오는 함수"""
|
||||
try:
|
||||
win32clipboard.OpenClipboard()
|
||||
if self.is_clipboard_image():
|
||||
dib_data = win32clipboard.GetClipboardData(win32clipboard.CF_DIB)
|
||||
image = Image.open(BytesIO(dib_data))
|
||||
return image
|
||||
else:
|
||||
self.logger.debug("클립보드에 이미지가 없습니다.")
|
||||
except Exception as e:
|
||||
self.logger.error(f"클립보드에서 이미지를 가져오는 중 오류 발생: {e}")
|
||||
finally:
|
||||
win32clipboard.CloseClipboard()
|
||||
|
||||
return None
|
||||
|
||||
def clear_clipboard(self):
|
||||
"""클립보드를 비우는 함수"""
|
||||
try:
|
||||
win32clipboard.OpenClipboard()
|
||||
win32clipboard.EmptyClipboard()
|
||||
self.logger.debug("클립보드가 비워졌습니다.")
|
||||
except Exception as e:
|
||||
self.logger.error(f"클립보드를 비우는 중 오류 발생: {e}")
|
||||
finally:
|
||||
win32clipboard.CloseClipboard()
|
||||
|
||||
def crop_image(self, image, crop_percentage=0.05):
|
||||
"""이미지를 주어진 퍼센트만큼 크롭하는 함수"""
|
||||
width, height = image.size
|
||||
left = width * crop_percentage
|
||||
top = height * crop_percentage
|
||||
right = width * (1 - crop_percentage)
|
||||
bottom = height * (1 - crop_percentage)
|
||||
|
||||
cropped_image = image.crop((left, top, right, bottom))
|
||||
|
||||
if self.debug:
|
||||
# 디버그 모드일 경우 크롭 전후 다양한 비율로 이미지 저장
|
||||
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||
original_image_path = os.path.join(os.getcwd(), f"original_image_{timestamp}.png")
|
||||
image.save(original_image_path) # 크롭 전 이미지 저장
|
||||
self.logger.debug(f"크롭 전 이미지 저장됨: {original_image_path}")
|
||||
|
||||
# 3%, 5%, 7% 크롭 이미지 저장
|
||||
crop_alternatives = [0.03, 0.05, 0.07]
|
||||
for crop in crop_alternatives:
|
||||
left_alt = width * crop
|
||||
top_alt = height * crop
|
||||
right_alt = width * (1 - crop)
|
||||
bottom_alt = height * (1 - crop)
|
||||
|
||||
cropped_alt_image = image.crop((left_alt, top_alt, right_alt, bottom_alt))
|
||||
cropped_image_path = os.path.join(os.getcwd(), f"cropped_image_{int(crop*100)}_{timestamp}.png")
|
||||
cropped_alt_image.save(cropped_image_path)
|
||||
self.logger.debug(f"{int(crop*100)}% 크롭된 이미지 저장됨: {cropped_image_path}")
|
||||
|
||||
return cropped_image
|
||||
|
||||
def set_image_to_clipboard(self, image):
|
||||
"""이미지를 클립보드에 넣는 함수"""
|
||||
output = BytesIO()
|
||||
image.save(output, "BMP")
|
||||
data = output.getvalue()[14:] # BMP 헤더 제거
|
||||
output.close()
|
||||
|
||||
try:
|
||||
win32clipboard.OpenClipboard()
|
||||
win32clipboard.EmptyClipboard()
|
||||
win32clipboard.SetClipboardData(win32clipboard.CF_DIB, data)
|
||||
self.logger.debug("이미지가 클립보드에 저장되었습니다.")
|
||||
except Exception as e:
|
||||
self.logger.error(f"이미지를 클립보드에 저장하는 중 오류 발생: {e}")
|
||||
finally:
|
||||
win32clipboard.CloseClipboard()
|
||||
|
||||
def base64_to_image(self, base64_data):
|
||||
"""Base64 데이터를 이미지로 변환하는 함수"""
|
||||
try:
|
||||
header, encoded = base64_data.split(',', 1)
|
||||
img_data = base64.b64decode(encoded)
|
||||
image = Image.open(BytesIO(img_data))
|
||||
return image
|
||||
except Exception as e:
|
||||
self.logger.error(f"Base64 이미지를 변환하는 중 오류 발생: {e}")
|
||||
return None
|
||||
351
gui.py
351
gui.py
|
|
@ -1,9 +1,11 @@
|
|||
from PyQt5.QtWidgets import QWidget, QPushButton, QVBoxLayout, QTextEdit, QLabel, QLineEdit, QHBoxLayout
|
||||
from PyQt5.QtCore import Qt, QRect, QSettings
|
||||
from PyQt5.QtWidgets import QWidget, QPushButton, QVBoxLayout, QTextEdit, QLabel, QLineEdit, QHBoxLayout, QProgressBar, QSizePolicy
|
||||
from PyQt5.QtCore import Qt, QRect, QSettings, QTimer
|
||||
from toggleSwitch import ToggleSwitch
|
||||
from browser_control import BrowserController
|
||||
from whale_translator import WhaleTranslator
|
||||
from base64_to_image import base64TOImage
|
||||
from clipboardImageManager import ClipboardImageManager
|
||||
from vertexAI import VertexAITranslator
|
||||
from option import OptionHandler
|
||||
from logger_module import QTextEditLogger # 추가
|
||||
import logging
|
||||
|
||||
|
|
@ -12,18 +14,42 @@ class TranslationApp(QWidget):
|
|||
super().__init__()
|
||||
self.initUI()
|
||||
self.logger = logger
|
||||
key_path = 'leensoo1nt.json'
|
||||
self.settings = QSettings("WhenRideMycar", "TranslationApp") # QSettings 초기화
|
||||
self.browser_controller = BrowserController(self, self.logger)
|
||||
self.whale_translator = WhaleTranslator(self, self.logger, debug_mode=False) # 디버그 모드 켜기
|
||||
self.base64TOImage = base64TOImage(self, logger, self.browser_controller)
|
||||
self.whale_translator = WhaleTranslator(self, self.logger, secret_mode=True,vd_mode=True) # 디버그 모드 켜기
|
||||
self.vertexAI = VertexAITranslator(self.logger, key_path)
|
||||
self.optionHandler = OptionHandler(self.browser_controller.page, self.logger, self.vertexAI)
|
||||
|
||||
self.clipboardImageManager = ClipboardImageManager(self, logger, self.browser_controller)
|
||||
|
||||
self.running = False
|
||||
|
||||
# 변수 설정
|
||||
self.start_time = 0
|
||||
self.finish_time = 0
|
||||
|
||||
self.total_product_count = 0
|
||||
self.current_product_count = 0
|
||||
|
||||
self.title_count = 0
|
||||
self.option_count = 0
|
||||
self.price_count = 0
|
||||
self.detail_image_count = 0
|
||||
self.thumb_image_count = 0
|
||||
|
||||
self.current_stage_index = 0 # 현재 진행 중인 단계 인덱스
|
||||
|
||||
|
||||
# 이전에 저장된 설정 불러오기
|
||||
self.load_settings()
|
||||
|
||||
# QTextEditLogger 추가
|
||||
# 로거 초기화
|
||||
self.add_text_edit_logger()
|
||||
|
||||
# 프로그래스바 초기화
|
||||
self.update_total_progress(0,0)
|
||||
|
||||
def add_text_edit_logger(self):
|
||||
"""QTextEdit에 로그를 출력하기 위한 핸들러 추가"""
|
||||
text_edit_logger = QTextEditLogger()
|
||||
|
|
@ -32,7 +58,40 @@ class TranslationApp(QWidget):
|
|||
text_edit_logger.appendHtml.connect(self.log.append) # appendHtml 대신 append로 수정
|
||||
text_edit_logger.scrollToBottom.connect(lambda: self.log.verticalScrollBar().setValue(self.log.verticalScrollBar().maximum()))
|
||||
self.logger.addHandler(text_edit_logger)
|
||||
self.logger.debug('QTextEditLogger가 설정되었습니다.')
|
||||
self.logger.debug('로그기록이 설정되었습니다.')
|
||||
|
||||
def start_stage(self, stage_index):
|
||||
"""지정한 단계에 깜빡임 효과 적용"""
|
||||
if 0 <= stage_index < len(self.stage_labels):
|
||||
self.timer = QTimer(self)
|
||||
self.blink_status = True
|
||||
self.timer.timeout.connect(lambda: self.blink_stage(stage_index))
|
||||
self.timer.start(500) # 0.5초 간격으로 깜빡임
|
||||
|
||||
def blink_stage(self, stage_index):
|
||||
"""지정한 단계의 색상을 주기적으로 변경하여 깜빡임 효과를 적용"""
|
||||
label = self.stage_labels[stage_index]
|
||||
if self.blink_status:
|
||||
label.setStyleSheet("background-color: yellow; padding: 5px;")
|
||||
else:
|
||||
label.setStyleSheet("background-color: lightgray; padding: 5px;")
|
||||
self.blink_status = not self.blink_status
|
||||
|
||||
def stop_blinking_effect(self):
|
||||
"""깜빡임 효과 중지"""
|
||||
self.timer.stop()
|
||||
|
||||
def complete_stage(self, stage_index):
|
||||
"""단계 완료 시 깜빡임을 중지하고 완료 상태로 변경"""
|
||||
if 0 <= stage_index < len(self.stage_labels):
|
||||
self.stop_blinking_effect()
|
||||
label = self.stage_labels[stage_index]
|
||||
label.setStyleSheet("background-color: green; padding: 5px;")
|
||||
self.current_stage_index += 1
|
||||
|
||||
# 다음 단계로 이동하여 깜빡임 시작
|
||||
if self.current_stage_index < len(self.stages):
|
||||
self.start_stage(self.current_stage_index)
|
||||
|
||||
def initUI(self):
|
||||
self.setWindowFlags(Qt.WindowStaysOnTopHint)
|
||||
|
|
@ -43,6 +102,31 @@ class TranslationApp(QWidget):
|
|||
self.log = QTextEdit(self)
|
||||
self.log.setReadOnly(True)
|
||||
|
||||
# 전체 프로그레스바
|
||||
self.total_progress_bar = QProgressBar(self)
|
||||
self.total_progress_bar.setValue(0)
|
||||
self.total_progress_bar.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
|
||||
|
||||
# 스테이지 타임라인
|
||||
self.stageTimeline_layout = QHBoxLayout()
|
||||
# self.stages = ["상품명", "옵션", "가격", "썸네일", "상페"]
|
||||
self.stages = ["옵션", "상페"]
|
||||
self.stage_labels = []
|
||||
for stage in self.stages:
|
||||
self.stage_layout = QHBoxLayout()
|
||||
label = QLabel(stage)
|
||||
label.setStyleSheet("background-color: lightgray; padding: 5px;")
|
||||
self.stage_labels.append(label)
|
||||
|
||||
self.stage_layout.addWidget(label)
|
||||
self.stageTimeline_layout.addLayout(self.stage_layout)
|
||||
|
||||
# 디테일 프로그레스바
|
||||
self.detail_progress_bar = QProgressBar(self)
|
||||
self.detail_progress_bar.setValue(0)
|
||||
self.detail_progress_bar.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
|
||||
self.detail_progress_bar.setVisible(False)
|
||||
|
||||
# 관리자 토글
|
||||
self.admin_toggle = ToggleSwitch(self)
|
||||
self.admin_toggle.clicked.connect(self.on_toggle_clicked)
|
||||
|
|
@ -50,6 +134,8 @@ class TranslationApp(QWidget):
|
|||
# 관리자 ID 및 PW
|
||||
self.admin_id_label = QLabel("관리자 ID:", self)
|
||||
self.admin_id_input = QLineEdit(self)
|
||||
|
||||
# 관리자 PW
|
||||
self.admin_pw_label = QLabel("관리자 PW:", self)
|
||||
self.admin_pw_input = QLineEdit(self)
|
||||
self.admin_pw_input.setEchoMode(QLineEdit.Password)
|
||||
|
|
@ -67,36 +153,61 @@ class TranslationApp(QWidget):
|
|||
self.pause_button = QPushButton('일시정지', self)
|
||||
self.exit_button = QPushButton('종료', self)
|
||||
|
||||
# 레이아웃 설정
|
||||
layout = QVBoxLayout()
|
||||
|
||||
# 관리자 토글 버튼 및 로그인 관련 필드 추가
|
||||
toggle_layout = QHBoxLayout()
|
||||
toggle_layout.addWidget(QLabel("관리자 여부:", self))
|
||||
toggle_layout.addWidget(self.admin_toggle)
|
||||
layout.addLayout(toggle_layout)
|
||||
# 버튼 크기를 1.5배로 설정
|
||||
button_height = int(self.start_chrome_button.sizeHint().height() * 1.5)
|
||||
self.start_chrome_button.setFixedHeight(button_height)
|
||||
self.translate_button.setFixedHeight(button_height)
|
||||
self.pause_button.setFixedHeight(button_height)
|
||||
self.exit_button.setFixedHeight(button_height)
|
||||
|
||||
# 관리자 ID/PW
|
||||
layout.addWidget(self.admin_id_label)
|
||||
layout.addWidget(self.admin_id_input)
|
||||
layout.addWidget(self.admin_pw_label)
|
||||
layout.addWidget(self.admin_pw_input)
|
||||
# 메인 레이아웃 설정
|
||||
self.main_layout = QVBoxLayout()
|
||||
|
||||
# 관리자 토글 버튼 및 로그인 관련 필드 추가
|
||||
self.toggle_layout = QHBoxLayout()
|
||||
self.toggle_layout.addWidget(QLabel("관리자 여부:", self))
|
||||
self.toggle_layout.addWidget(self.admin_toggle)
|
||||
self.main_layout.addLayout(self.toggle_layout,1)
|
||||
|
||||
# 관리자 ID
|
||||
self.main_layout.addWidget(self.admin_id_label)
|
||||
self.main_layout.addWidget(self.admin_id_input)
|
||||
|
||||
# 관리자 PW
|
||||
self.admin_layout = QVBoxLayout()
|
||||
self.admin_layout.addWidget(self.admin_pw_label)
|
||||
self.admin_layout.addWidget(self.admin_pw_input)
|
||||
|
||||
# 직원 ID/PW
|
||||
layout.addWidget(self.user_id_label)
|
||||
layout.addWidget(self.user_id_input)
|
||||
layout.addWidget(self.user_pw_label)
|
||||
layout.addWidget(self.user_pw_input)
|
||||
self.user_layout = QVBoxLayout()
|
||||
self.user_layout.addWidget(self.user_id_label)
|
||||
self.user_layout.addWidget(self.user_id_input)
|
||||
self.user_layout.addWidget(self.user_pw_label)
|
||||
self.user_layout.addWidget(self.user_pw_input)
|
||||
|
||||
# 관리자와 직원 레이아웃을 메인 레이아웃에 추가
|
||||
self.main_layout.addLayout(self.admin_layout,3)
|
||||
self.main_layout.addLayout(self.user_layout,3)
|
||||
|
||||
# 크롬 및 번역 관련 버튼
|
||||
layout.addWidget(self.start_chrome_button)
|
||||
layout.addWidget(self.translate_button)
|
||||
layout.addWidget(self.pause_button)
|
||||
layout.addWidget(self.exit_button)
|
||||
self.setLayout(layout)
|
||||
self.button_layout = QHBoxLayout()
|
||||
self.button_layout.addWidget(self.start_chrome_button)
|
||||
self.button_layout.addWidget(self.translate_button)
|
||||
self.button_layout.addWidget(self.pause_button)
|
||||
self.button_layout.addWidget(self.exit_button)
|
||||
|
||||
layout.addWidget(self.log)
|
||||
# 로그 및 프로그레스바 레이아웃
|
||||
self.log_layout = QVBoxLayout()
|
||||
self.log_layout.addWidget(self.log)
|
||||
self.log_layout.addWidget(self.total_progress_bar)
|
||||
self.log_layout.addLayout(self.stageTimeline_layout)
|
||||
self.log_layout.addWidget(self.detail_progress_bar)
|
||||
|
||||
# 메인 레이아웃에 버튼 레이아웃과 로그 레이아웃 추가
|
||||
self.main_layout.addLayout(self.button_layout,2)
|
||||
self.main_layout.addLayout(self.log_layout,5)
|
||||
|
||||
self.setLayout(self.main_layout)
|
||||
|
||||
# 기본 상태 설정
|
||||
self.on_toggle_clicked(False)
|
||||
|
|
@ -108,27 +219,23 @@ class TranslationApp(QWidget):
|
|||
self.exit_button.clicked.connect(self.close)
|
||||
|
||||
def on_toggle_clicked(self, is_checked):
|
||||
"""관리자 토글 상태에 따라 필드 활성화/비활성화"""
|
||||
"""관리자 토글 상태에 따라 관리자와 직원 필드를 표시/숨김"""
|
||||
if is_checked:
|
||||
# 관리자 모드
|
||||
# self.admin_id_label.setVisible(True)
|
||||
# self.admin_id_input.setVisible(True)
|
||||
self.admin_pw_label.setVisible(True)
|
||||
self.admin_pw_input.setVisible(True)
|
||||
self.user_id_label.setVisible(False)
|
||||
self.user_id_input.setVisible(False)
|
||||
self.user_pw_label.setVisible(False)
|
||||
self.user_pw_input.setVisible(False)
|
||||
# 관리자 모드: 직원 레이아웃을 숨기고, 관리자 PW를 표시
|
||||
self.set_layout_visibility(self.admin_layout, True)
|
||||
self.set_layout_visibility(self.user_layout, False)
|
||||
else:
|
||||
# 직원 모드
|
||||
# self.admin_id_label.setVisible(False)
|
||||
# self.admin_id_input.setVisible(False)
|
||||
self.admin_pw_label.setVisible(False)
|
||||
self.admin_pw_input.setVisible(False)
|
||||
self.user_id_label.setVisible(True)
|
||||
self.user_id_input.setVisible(True)
|
||||
self.user_pw_label.setVisible(True)
|
||||
self.user_pw_input.setVisible(True)
|
||||
# 직원 모드: 관리자 PW를 숨기고, 직원 레이아웃을 표시
|
||||
self.set_layout_visibility(self.admin_layout, False)
|
||||
self.set_layout_visibility(self.user_layout, True)
|
||||
|
||||
def set_layout_visibility(self, changelayout, visible):
|
||||
"""레이아웃에 포함된 모든 위젯의 가시성을 설정"""
|
||||
for i in range(changelayout.count()):
|
||||
widget = changelayout.itemAt(i).widget()
|
||||
if widget:
|
||||
widget.setVisible(visible)
|
||||
|
||||
|
||||
def start_browser(self):
|
||||
"""크롬 브라우저 실행 후 로그인"""
|
||||
|
|
@ -171,23 +278,48 @@ class TranslationApp(QWidget):
|
|||
self.admin_toggle.setChecked(admin_toggle_state)
|
||||
self.on_toggle_clicked(admin_toggle_state)
|
||||
|
||||
def update_total_progress(self, current_value, total_value):
|
||||
|
||||
if current_value == 0:
|
||||
self.total_progress_bar.setValue(0)
|
||||
self.total_progress_bar.setFormat("상품 수정 대기") # current_value가 0일 때 표시될 텍스트
|
||||
else:
|
||||
# 프로그레스바의 값과 텍스트를 설정
|
||||
percentage = int((current_value / total_value) * 100)
|
||||
self.total_progress_bar.setValue(percentage)
|
||||
self.total_progress_bar.setFormat(f"상품 {current_value}/{total_value}개 완료 [{percentage}%]")
|
||||
|
||||
def update_detail_progress(self, current_value, total_value):
|
||||
|
||||
if current_value == 0:
|
||||
self.detail_progress_bar.setValue(0)
|
||||
self.detail_progress_bar.setFormat("수정 대기") # current_value가 0일 때 표시될 텍스트
|
||||
else:
|
||||
# 프로그레스바의 값과 텍스트를 설정
|
||||
percentage = int((current_value / total_value) * 100)
|
||||
self.detail_progress_bar.setValue(percentage)
|
||||
self.detail_progress_bar.setFormat(f"{current_value}/{total_value}개 완료 [{percentage}%]")
|
||||
|
||||
def start_translation(self):
|
||||
self.logger.debug('번역 작업을 시작합니다...')
|
||||
self.running = True # 번역 작업이 시작됨
|
||||
|
||||
try:
|
||||
# # 1. 광고 다이얼로그가 나타날 경우 닫기 처리
|
||||
# self.logger.debug('광고 다이얼로그 닫기 처리 중...')
|
||||
# self.browser_controller.close_ad_dialog_if_present()
|
||||
|
||||
# 2. "신규 상품 등록" 페이지로 이동
|
||||
# 1. "신규 상품 등록" 페이지로 이동
|
||||
self.logger.debug('신규 상품 등록 페이지로 이동 중...')
|
||||
self.browser_controller.go_to_new_product_page()
|
||||
|
||||
# # Playwright에서 페이지 스크롤 후 "세부사항 수정 및 업로드" 버튼 수집
|
||||
# self.browser_controller.scroll_page_to_bottom()
|
||||
# 2. 총 상품 수 수집
|
||||
total_products = self.browser_controller.get_total_product_count()
|
||||
if total_products == 0:
|
||||
self.logger.debug('수집할 상품이 없습니다. 작업을 종료합니다.')
|
||||
return
|
||||
|
||||
self.total_progress_bar.setMaximum(total_products)
|
||||
self.total_progress_bar.setValue(0)
|
||||
completed_count = 0
|
||||
self.update_total_progress(completed_count,total_products)
|
||||
|
||||
# 3. 각 상품에 대해 "세부사항 수정 및 업로드" 작업을 수행
|
||||
page_number = 1
|
||||
while self.running:
|
||||
self.logger.debug(f'현재 페이지: {page_number}')
|
||||
|
|
@ -207,28 +339,31 @@ class TranslationApp(QWidget):
|
|||
|
||||
self.logger.debug(f'{index}/{len(product_buttons)}: 세부사항 수정 작업 중...')
|
||||
|
||||
# 상품명 수집 및 수집오류 처리
|
||||
product_name = self.browser_controller.get_product_name(index)
|
||||
if product_name == "수집 오류 발생":
|
||||
self.logger.debug('상품 수집 오류, 다음 상품으로 넘어갑니다.')
|
||||
continue
|
||||
|
||||
# 상품 수정 다이얼로그 열기
|
||||
self.browser_controller.open_product_edit_dialog(button)
|
||||
|
||||
# 상세페이지 탭 클릭
|
||||
self.browser_controller.click_detail_tab()
|
||||
|
||||
# 이미지 URL 추출
|
||||
image_urls = self.browser_controller.extract_image_urls()
|
||||
|
||||
# 이미지 번역 작업 진행
|
||||
for url in image_urls:
|
||||
if not self.running:
|
||||
self.logger.debug('번역 작업이 중단되었습니다.')
|
||||
break
|
||||
|
||||
self.whale_translator.translate_image(url)
|
||||
self.browser_controller.paste_image_in_chrome(self.base64TOImage, url)
|
||||
# 옵션 수정
|
||||
self.start_stage(0)
|
||||
self.edit_option()
|
||||
self.complete_stage(0)
|
||||
|
||||
# 상세페이지 수정
|
||||
self.start_stage(1)
|
||||
self.detail_trans()
|
||||
self.complete_stage(1)
|
||||
|
||||
# 수정 후 저장
|
||||
self.logger.debug('상품 세부사항 저장 중...')
|
||||
self.browser_controller.save_product_edit()
|
||||
|
||||
completed_count += 1
|
||||
self.update_total_progress(completed_count,total_products)
|
||||
self.logger.debug('상품 수정 완료.')
|
||||
|
||||
# 6. 다음 페이지로 이동 (있으면)
|
||||
|
|
@ -245,30 +380,6 @@ class TranslationApp(QWidget):
|
|||
self.logger.debug(f'번역 작업 중 오류 발생: {str(e)}')
|
||||
self.running = False
|
||||
|
||||
def start_translation_by_one(self):
|
||||
self.logger.debug('번역 작업을 시작합니다...')
|
||||
self.running = True # 번역 작업이 시작됨
|
||||
|
||||
# Playwright에서 이미지 URL 추출
|
||||
image_urls = self.browser_controller.extract_image_urls()
|
||||
|
||||
# 추출된 URL을 WhaleTranslator에 전달하여 번역 진행
|
||||
for url in image_urls:
|
||||
if not self.running: # 작업이 중단되었는지 확인
|
||||
self.logger.debug('번역 작업이 중단되었습니다.')
|
||||
break
|
||||
|
||||
self.whale_translator.translate_image(url)
|
||||
# 번역 후 크롬으로 포커스를 옮기고 이미지를 붙여넣기
|
||||
self.browser_controller.paste_image_in_chrome(self.base64TOImage)
|
||||
|
||||
page = BrowserController.get_page()
|
||||
page.click('button:has-text("저장하기")') # 모든 이미지 번역 완료 후 저장하기 클릭
|
||||
|
||||
if self.running:
|
||||
self.logger.debug('모든 이미지 번역 및 붙여넣기 완료.')
|
||||
self.running = False # 작업 종료 후 상태를 False로 전환
|
||||
|
||||
def pause_translation(self):
|
||||
self.logger.debug('번역 작업을 중단합니다...')
|
||||
self.running = False # 번역 작업 중단
|
||||
|
|
@ -279,3 +390,53 @@ class TranslationApp(QWidget):
|
|||
self.browser_controller.close_browser() # 브라우저 종료
|
||||
self.whale_translator.close_all_virtual_desktops()
|
||||
super().close()
|
||||
|
||||
def detail_trans(self):
|
||||
# 상세페이지 탭 클릭
|
||||
self.browser_controller.click_detail_tab()
|
||||
self.detail_progress_bar.setValue(0)
|
||||
self.detail_progress_bar.setVisible(True)
|
||||
|
||||
# 이미지 URL 추출
|
||||
image_urls = self.browser_controller.extract_image_urls()
|
||||
total_images = len(image_urls)
|
||||
self.logger.debug(f"현재 상품의 총 이미지 수 : {total_images}개")
|
||||
|
||||
self.detail_image_count += total_images
|
||||
|
||||
# 이미지 번역 작업 진행
|
||||
for url, i in enumerate(image_urls):
|
||||
current_image_count = i +1
|
||||
|
||||
if not self.running:
|
||||
self.logger.debug('번역 작업이 중단되었습니다.')
|
||||
break
|
||||
|
||||
self.whale_translator.translate_image(url)
|
||||
self.browser_controller.paste_image_in_chrome(self.clipboardImageManager, url)
|
||||
self.update_detail_progress(i,total_images)
|
||||
|
||||
current_image_count += 1
|
||||
|
||||
# 수정 후 저장
|
||||
self.logger.debug('상품 세부사항 저장 중...')
|
||||
self.browser_controller.save_product_edit()
|
||||
|
||||
self.detail_progress_bar.setVisible(False)
|
||||
self.detail_progress_bar.setValue(0)
|
||||
|
||||
def edit_option(self):
|
||||
# 상세페이지 탭 클릭
|
||||
self.browser_controller.click_option_tab()
|
||||
self.detail_progress_bar.setVisible(True)
|
||||
|
||||
# 옵션 최대선택갯수
|
||||
max_option_count = 10
|
||||
self.optionHandler.process_options(max_option_count)
|
||||
|
||||
# 수정 후 저장
|
||||
self.logger.debug('상품 세부사항 저장 중...')
|
||||
self.browser_controller.save_product_edit()
|
||||
|
||||
self.detail_progress_bar.setVisible(False)
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"type": "service_account",
|
||||
"project_id": "igneous-primacy-409723",
|
||||
"private_key_id": "9a9816ba7d7bcde45bc1f0f0f984586ad753022d",
|
||||
"private_key": "-----BEGIN PRIVATE KEY-----\nMIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCzx0uJieV+r8PU\ntdVrWrbpbMOXR0SIhCVKkM1GWEOF+p6c5Hd3WZz09ALdBWtIQkGNIodzwO4nKh3h\n9F0dNviY6zB86co6UkpdivOm+w6EOBA0qQSF5dCQsvsJ6FyWqZgNPBfZonlk96Hy\nCNtNbkUqxTEPXJQ751WiH0Ke9e6jhPQ9g2ORmDrW1ANNGD4r1dz9GUEegWkFouGE\nsjPgwXEO62i3cFxUuRNMFEE/bvIB/VNLYMqbo4osGfTXopl9N+lW0MJ0sINVnZaI\nImnU6I6F9NMRb1PjkXD/FsLl6lbE82dbZDTdq11rjA7KQqBNXne58DVwE3/dmowi\n3NaGdqkjAgMBAAECggEAAZbnNTXPeaBoxR5kKBbUFyw93Cv4kEoj82g/w8wGQ6og\nKE5hYATCz6MK8ScHrzhv4snX75hYgrAiWlvtltNkc9o75vEcgJwSyfWBr7BBsb4k\nAffBKDJ6P9+MFl3EIYMpxiHlT4Pag3sob/Wq8Y6ZK6sQXkOLR2VzD80G9AxEBZTt\n1Asa9lBXSnK5NCYLsRRX765YysrsQXd8tk6oqsLmcigWCr75nKgbELxiszOTKtLT\npb50y8cb0kDfMUXOltWojGIkhOKetbzmWA3EgXIIa75s9z8tUVE+vO6kSq3TuQXH\nwL+IwatWRlrco3G1LGsHtw2nPR2mljcCVZjvT/Z0gQKBgQDmOJIeobqku/bVDxcq\neLYrMV1/X7zTihox33C7w+ruVXB/edJxR+gROWVO9ufTIgfPTzkDkxZYvTiHA9hC\nz56vu1b2RNLgiGT9ASOvfR00xRSCE/FfaaN5VlWzGskuFUyWDthkbld78wEFLf19\nutsaV/9+RCGZNoGtRjjw4symSQKBgQDH6MI+wXejhAJyZrr2S5jvIlSKtY12QdU6\n+JhD+3OEBl+OndyfucD5HgjSJnMjnzRMML+mPFlcwqU1VmDeeTqSE/mmvyRAr9rm\nG6Xdh+dOngpqwpq9OGsqc+JZ8JF0bdn6V/g26LjkQgNnRCIFnARQ2UHBDoS2/wHn\n72ShlP9kCwKBgGlinADJp9ag9Gyza7dVao57GoGkIZv0K+mIjuJk3LYdBlJUQbD5\naZH45Bcxjw1nFowfh8nLGv+kHqwvZl+vCsUGzNgOyTlfNltamitK6oOtc6XX2zYB\n9YMlsjU6nb0qotROF2Bh4korAtyMIO3dC08T2TDDn12zRck7y/T43RWBAoGALEEO\nny3c+knC8OhlAxkBJg8HgB1oz4ELXx6hNot3qwZuKPgxWvqYCY3ojf0NCBm6ThOM\nmZRKhApi4Efa8eUMXkIlxhASSm+jmcUNFtl7DyBVVgT2lGTk9GTq+tYSnR+kXZMT\n07P5Gi6y6i1fCrbbDbrKn55DKu+Q0HNiZ5LAZrkCgYAp7Aa1rlbXbyoSXNWTA4Nc\nTZIRKj9Ra2hD2Y2EKjLWqLa9RVK+D9a9I/v1gW59PeRpUY9w674IEZXqOq5jx0D9\nFmwL00Omtfv96q+syq7pqSUmI7hDSd1CfLDaxCGHzGykI98GkjQDz6xPEgTAKRIL\njcB1KaEd55AuovwONS3qKA==\n-----END PRIVATE KEY-----\n",
|
||||
"client_email": "service-account@igneous-primacy-409723.iam.gserviceaccount.com",
|
||||
"client_id": "102875157826238718143",
|
||||
"auth_uri": "https://accounts.google.com/o/oauth2/auth",
|
||||
"token_uri": "https://oauth2.googleapis.com/token",
|
||||
"auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
|
||||
"client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/service-account%40igneous-primacy-409723.iam.gserviceaccount.com",
|
||||
"universe_domain": "googleapis.com"
|
||||
}
|
||||
|
|
@ -0,0 +1,199 @@
|
|||
class OptionHandler:
|
||||
def __init__(self, page, logger, vertexAI):
|
||||
self.page = page
|
||||
self.logger = logger
|
||||
self.vertexAI = vertexAI
|
||||
|
||||
def process_options(self):
|
||||
"""옵션 상품을 처리하는 메서드"""
|
||||
try:
|
||||
# 1. 단일 옵션인지 판단
|
||||
if self.is_single_option():
|
||||
self.logger.debug("단일 옵션 상품입니다. 옵션 수정 과정을 생략합니다.")
|
||||
return
|
||||
|
||||
# 2. 전체 옵션 체크박스 상태 확인
|
||||
if not self.is_all_options_checked():
|
||||
self.logger.debug("옵션이 일부만 체크된 상태입니다. 옵션 수정이 완료된 상품으로 판단하여 패스합니다.")
|
||||
return
|
||||
|
||||
# 3. 가격 낮은 순 정렬 클릭
|
||||
self.logger.debug("가격 낮은 순 정렬을 클릭합니다.")
|
||||
self.page.click('button:has-text("가격 낮은 순")')
|
||||
self.page.wait_for_load_state('domcontentloaded')
|
||||
|
||||
# 4. 옵션 정보 수집 및 번역
|
||||
option_info = self.collect_options_info()
|
||||
|
||||
# Vertex AI를 통해 옵션명을 번역
|
||||
self.logger.debug(f"수집된 원본 옵션 정보: {option_info['original_names']}")
|
||||
translated_options = self.vertexAI.translate_options(option_info['original_names'])
|
||||
self.logger.debug(f"번역된 옵션 정보: {translated_options}")
|
||||
|
||||
# 5. 번역된 옵션명 편집칸에 입력
|
||||
self.logger.debug("번역된 옵션명을 입력합니다.")
|
||||
self.apply_translated_options(translated_options, option_info['edit_fields'])
|
||||
|
||||
# 6. 옵션 선택 및 제한 처리
|
||||
self.adjust_options(option_info['checkboxes'])
|
||||
|
||||
# 7. 저장 버튼 클릭
|
||||
self.logger.debug("저장 버튼을 클릭합니다.")
|
||||
self.page.click('button:has-text("저장하기")')
|
||||
|
||||
self.logger.debug("옵션 처리 완료.")
|
||||
|
||||
except Exception as e:
|
||||
self.logger.debug(f"옵션 처리 중 오류 발생: {str(e)}")
|
||||
return
|
||||
|
||||
def is_single_option(self):
|
||||
"""단일 옵션 상품 여부를 확인"""
|
||||
try:
|
||||
radio_group = self.page.query_selector('div.ant-row.css-1li46mu')
|
||||
option_radio = radio_group.query_selector("//label[span[text()='옵션 상품등록']]//input[@type='radio']")
|
||||
single_radio = radio_group.query_selector("//label[span[text()='단일 상품등록']]//input[@type='radio']")
|
||||
return single_radio.is_checked() and not option_radio.is_checked()
|
||||
except Exception as e:
|
||||
self.logger.debug(f"단일 옵션 확인 중 오류 발생: {str(e)}")
|
||||
return False
|
||||
|
||||
def is_all_options_checked(self):
|
||||
"""전체 옵션 체크박스 상태를 확인 (전체 체크 여부)"""
|
||||
try:
|
||||
checkbox = self.page.query_selector('#productMainContentContainerId .ant-checkbox-wrapper-checked')
|
||||
if checkbox:
|
||||
return True
|
||||
checkbox_partial = self.page.query_selector('#productMainContentContainerId .ant-checkbox-indeterminate')
|
||||
return checkbox_partial is None # 일부 체크 시 False
|
||||
except Exception as e:
|
||||
self.logger.debug(f"전체 옵션 체크박스 확인 중 오류 발생: {str(e)}")
|
||||
return False
|
||||
|
||||
def collect_options_info(self):
|
||||
"""옵션 정보를 수집 (이미지, 옵션명, 편집 필드, 가격, 체크박스 정보 포함)"""
|
||||
option_info = {
|
||||
'original_names': {},
|
||||
'edit_fields': {},
|
||||
'checkboxes': [],
|
||||
'images': {},
|
||||
'prices': {} # 가격 정보 추가
|
||||
}
|
||||
|
||||
try:
|
||||
# 총 옵션 갯수 수집
|
||||
total_options_selector = '#productMainContentContainerId label.ant-checkbox-wrapper'
|
||||
total_options_element = self.page.query_selector(total_options_selector)
|
||||
if total_options_element:
|
||||
total_options_text = total_options_element.inner_text()
|
||||
total_options_count = int(''.join(filter(str.isdigit, total_options_text))) # 숫자만 추출
|
||||
else:
|
||||
total_options_count = 0 # 옵션 갯수를 찾지 못할 경우 기본값
|
||||
|
||||
self.logger.debug(f"총 옵션 갯수: {total_options_count}")
|
||||
|
||||
# 옵션 정보를 수집 (총 옵션 갯수만큼 반복)
|
||||
for i in range(1, total_options_count + 1):
|
||||
try:
|
||||
# 옵션명 수집
|
||||
original_name_selector = f'#productMainContentContainerId li:nth-child({i}) .Body3Regular14'
|
||||
original_name_element = self.page.query_selector(original_name_selector)
|
||||
original_name = original_name_element.inner_text() if original_name_element else None
|
||||
|
||||
if original_name:
|
||||
# 옵션명 기준으로 수집 항목 구성
|
||||
option_info['original_names'][f'origin_option_{i}'] = original_name
|
||||
|
||||
# 옵션 편집 필드 수집
|
||||
edit_field_selector = f'#productMainContentContainerId li:nth-child({i}) input.ant-input'
|
||||
edit_field_element = self.page.query_selector(edit_field_selector)
|
||||
if edit_field_element:
|
||||
option_info['edit_fields'][original_name] = edit_field_element
|
||||
|
||||
# 옵션 체크박스 수집
|
||||
checkbox_selector = f'#productMainContentContainerId li:nth-child({i}) input[type="checkbox"]'
|
||||
checkbox_element = self.page.query_selector(checkbox_selector)
|
||||
if checkbox_element:
|
||||
option_info['checkboxes'].append(checkbox_element)
|
||||
|
||||
# 옵션 이미지 수집
|
||||
image_selector = f'#productMainContentContainerId li:nth-child({i}) img.sc-gbvfcU.ezktkd'
|
||||
image_element = self.page.query_selector(image_selector)
|
||||
if image_element:
|
||||
image_url = image_element.get_attribute('src')
|
||||
option_info['images'][original_name] = image_url
|
||||
else:
|
||||
option_info['images'][original_name] = None # 이미지가 없으면 None
|
||||
|
||||
# 가격 정보 수집
|
||||
price_selector = f'#productMainContentContainerId li:nth-child({i}) sup'
|
||||
price_element = self.page.query_selector(price_selector)
|
||||
if price_element:
|
||||
price_text = price_element.inner_text().replace(",", "").replace("원", "").strip()
|
||||
if " - " in price_text:
|
||||
low_price, high_price = map(int, price_text.split(" - "))
|
||||
else:
|
||||
low_price = high_price = int(price_text)
|
||||
option_info['prices'][original_name] = {'low_price': low_price, 'high_price': high_price}
|
||||
|
||||
except Exception as e:
|
||||
self.logger.debug(f"옵션 {i} 수집 중 오류 발생: {str(e)}")
|
||||
|
||||
except Exception as e:
|
||||
self.logger.debug(f"옵션 정보 수집 중 오류 발생: {str(e)}")
|
||||
|
||||
return option_info
|
||||
|
||||
def apply_translated_options(self, translated_options, edit_fields):
|
||||
"""번역된 옵션명을 편집칸에 입력"""
|
||||
try:
|
||||
for key, translated_name in translated_options.items():
|
||||
option_number = key.split('_')[-1] # 'trans_option_1'에서 '1' 추출
|
||||
edit_field = edit_fields.get(f'edit_option_{option_number}')
|
||||
if edit_field:
|
||||
edit_field.fill(translated_name)
|
||||
except Exception as e:
|
||||
self.logger.debug(f"번역된 옵션명을 입력하는 중 오류 발생: {str(e)}")
|
||||
|
||||
def adjust_options(self, checkboxes):
|
||||
"""옵션 체크 상태 조정"""
|
||||
try:
|
||||
if len(checkboxes) > 3:
|
||||
self.logger.debug("옵션이 3개 이상이므로 가장 낮은 옵션을 체크 해제합니다.")
|
||||
checkboxes[0].click()
|
||||
|
||||
if len(checkboxes) > 10:
|
||||
self.logger.debug("옵션이 10개 이상이므로 초과 옵션을 체크 해제합니다.")
|
||||
for i in range(10, len(checkboxes)):
|
||||
checkboxes[i].click()
|
||||
except Exception as e:
|
||||
self.logger.debug(f"옵션 체크 조정 중 오류 발생: {str(e)}")
|
||||
|
||||
def check_options(self, option_info):
|
||||
"""옵션 체크 로직: 모든 옵션 체크 해제 후 다시 선택"""
|
||||
try:
|
||||
# 전체 옵션 체크박스 체크 해제
|
||||
total_checkbox_selector = '#productMainContentContainerId label.ant-checkbox-wrapper'
|
||||
total_checkbox_element = self.page.query_selector(total_checkbox_selector)
|
||||
if total_checkbox_element:
|
||||
total_checkbox_element.click()
|
||||
self.logger.debug("모든 옵션 체크 해제 완료")
|
||||
|
||||
# 옵션 갯수에 따라 선택 로직 진행
|
||||
total_options_count = len(option_info['original_names'])
|
||||
self.logger.debug(f"선택 가능한 옵션 수: {total_options_count}")
|
||||
|
||||
if total_options_count > 2:
|
||||
# 3개 이상인 경우: 1번째 옵션을 제외하고 최대 10개까지만 체크
|
||||
options_to_check = option_info['checkboxes'][1:self.max_selected_options + 1]
|
||||
else:
|
||||
# 2개 이하인 경우: 모두 체크
|
||||
options_to_check = option_info['checkboxes']
|
||||
|
||||
# 선택된 옵션들 체크
|
||||
for checkbox in options_to_check:
|
||||
checkbox.click()
|
||||
self.logger.debug(f"옵션 체크 완료: {checkbox}")
|
||||
|
||||
except Exception as e:
|
||||
self.logger.debug(f"옵션 체크 중 오류 발생: {str(e)}")
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
{
|
||||
"option_prompt_template": "질문은 아래와 같아.\n\n가공방법\n1. 특수문자가 있을 경우 제거해줘.\n2. 원본 상품명을 참고해서, 각 옵션의 이름을 최대한 간결하게, 각 옵션의 특징만 남겨줘.\n3. 간결하게 만들어진 각 옵션명을 한국어로 일관되게 번역해줘.\n4. 번역된 옵션명들은 'trans_option_1': '', 'trans_option_2': '', 'trans_option_3': '', 'trans_option_4': '' 와 같은 형식(json)으로 반환해줘.\n\n원본 데이터\n{options}",
|
||||
"detail_page_prompt_template": "상세 페이지 번역 요청: {detail_page}",
|
||||
"title_prompt_template": "제목 번역 요청: {title}",
|
||||
"price_prompt_template": "가격 가공 요청: {price}",
|
||||
"allowed_special_chars": "!$~()._-=+/",
|
||||
"special_char_replacements":{
|
||||
"*" : "X",
|
||||
"【" : "(",
|
||||
"】" : ")",
|
||||
"[" : "(",
|
||||
"]" : ")",
|
||||
"," : "."
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,210 @@
|
|||
import sys
|
||||
import asyncio
|
||||
from PyQt5.QtWidgets import (QApplication, QWidget, QVBoxLayout, QLabel,
|
||||
QLineEdit, QPushButton, QComboBox, QTextEdit, QFileDialog, QHBoxLayout)
|
||||
from PyQt5.QtCore import Qt
|
||||
from playwright.async_api import async_playwright
|
||||
from PyQt5.QtGui import QClipboard
|
||||
|
||||
class PlaywrightTester(QWidget):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
# GUI 레이아웃 설정
|
||||
self.initUI()
|
||||
self.browser = None
|
||||
self.page = None
|
||||
self.elements = [] # 여러 요소를 찾을 때 저장하는 리스트
|
||||
self.current_element_index = 0 # 현재 확인 중인 요소 인덱스
|
||||
self.playwright = None
|
||||
self.log_file_path = None
|
||||
self.clipboard = QApplication.clipboard()
|
||||
|
||||
def initUI(self):
|
||||
layout = QVBoxLayout()
|
||||
|
||||
# 테스트할 URL 입력 필드
|
||||
self.url_input = QLineEdit(self)
|
||||
self.url_input.setPlaceholderText("Enter URL to test")
|
||||
self.url_input.setText("https://percenty.co.kr")
|
||||
layout.addWidget(self.url_input)
|
||||
|
||||
# 선택자 타입 설명 라벨
|
||||
self.selector_type_label = QLabel("Selector Type (CSS, XPath, Name):")
|
||||
layout.addWidget(self.selector_type_label)
|
||||
|
||||
# 선택자 타입 콤보박스 (CSS, XPath, Name)
|
||||
self.selector_type_combobox = QComboBox()
|
||||
self.selector_type_combobox.addItems(["CSS", "XPath", "Name"])
|
||||
self.selector_type_combobox.currentIndexChanged.connect(self.update_selector_input)
|
||||
layout.addWidget(self.selector_type_combobox)
|
||||
|
||||
# 선택자 입력 필드
|
||||
self.selector_input = QLineEdit(self)
|
||||
self.selector_input.setPlaceholderText("Enter selector here (CSS, XPath, Name)")
|
||||
layout.addWidget(self.selector_input)
|
||||
|
||||
# Playwright 실행 버튼
|
||||
self.playwright_button = QPushButton("Start Playwright", self)
|
||||
self.playwright_button.clicked.connect(self.start_playwright)
|
||||
layout.addWidget(self.playwright_button)
|
||||
|
||||
# 요소 찾기 버튼
|
||||
self.find_button = QPushButton("Find Element", self)
|
||||
self.find_button.clicked.connect(self.find_element)
|
||||
layout.addWidget(self.find_button)
|
||||
|
||||
# 요소 클릭 버튼
|
||||
self.click_button = QPushButton("Click Element", self)
|
||||
self.click_button.clicked.connect(self.click_element)
|
||||
layout.addWidget(self.click_button)
|
||||
|
||||
# 다음 요소 확인 버튼 (여러 요소가 있을 때)
|
||||
self.next_element_button = QPushButton("Next Element", self)
|
||||
self.next_element_button.clicked.connect(self.show_next_element)
|
||||
layout.addWidget(self.next_element_button)
|
||||
|
||||
# 선택자 복사 버튼
|
||||
self.copy_selector_button = QPushButton("Copy Selector", self)
|
||||
self.copy_selector_button.clicked.connect(self.copy_selector)
|
||||
layout.addWidget(self.copy_selector_button)
|
||||
|
||||
# 로그 창 (출력 결과 표시)
|
||||
self.log_output = QTextEdit(self)
|
||||
self.log_output.setReadOnly(True)
|
||||
layout.addWidget(self.log_output)
|
||||
|
||||
# 메인 레이아웃 설정
|
||||
self.setLayout(layout)
|
||||
self.setWindowTitle('Playwright Element Tester')
|
||||
self.setGeometry(300, 300, 400, 400)
|
||||
|
||||
def log_message(self, message):
|
||||
"""로그 출력 및 저장"""
|
||||
self.log_output.append(message)
|
||||
|
||||
def update_selector_input(self):
|
||||
"""선택자 타입에 따라 입력 필드를 업데이트"""
|
||||
selector_type = self.selector_type_combobox.currentText()
|
||||
if selector_type == "Name":
|
||||
self.selector_input.setPlaceholderText("Enter text to find elements by Name")
|
||||
else:
|
||||
self.selector_input.setPlaceholderText(f"Enter {selector_type} selector here")
|
||||
|
||||
async def async_playwright_setup(self, url):
|
||||
"""Playwright 비동기 실행 및 페이지 설정"""
|
||||
self.playwright = await async_playwright().start()
|
||||
self.browser = await self.playwright.chromium.launch(headless=False)
|
||||
|
||||
# 페이지 생성 시 viewport 설정
|
||||
self.page = await self.browser.new_page(viewport={"width": 1920, "height": 1080})
|
||||
await self.page.goto(url)
|
||||
|
||||
def start_playwright(self):
|
||||
"""Playwright 실행"""
|
||||
url = self.url_input.text()
|
||||
if not url:
|
||||
self.log_message("URL을 입력하세요.")
|
||||
return
|
||||
|
||||
self.log_message(f"Playwright 실행 중... URL: {url}")
|
||||
loop = asyncio.get_event_loop()
|
||||
loop.run_until_complete(self.async_playwright_setup(url))
|
||||
self.log_message("Playwright 실행 완료. 페이지 로드됨.")
|
||||
|
||||
async def find_element_async(self):
|
||||
"""요소 찾기 비동기 작업"""
|
||||
selector_type = self.selector_type_combobox.currentText()
|
||||
selector = self.selector_input.text()
|
||||
|
||||
try:
|
||||
self.elements = [] # 요소 리스트 초기화
|
||||
if selector_type == "CSS":
|
||||
self.elements = await self.page.query_selector_all(selector)
|
||||
elif selector_type == "XPath":
|
||||
self.elements = await self.page.query_selector_all(selector)
|
||||
elif selector_type == "Name":
|
||||
# 이름을 기준으로 요소 찾기
|
||||
self.elements = await self.page.query_selector_all(f'[name="{selector}"]')
|
||||
|
||||
if self.elements:
|
||||
self.current_element_index = 0
|
||||
self.log_message(f"요소 찾기 성공. 총 {len(self.elements)}개의 요소 발견.")
|
||||
await self.show_current_element_info()
|
||||
else:
|
||||
self.log_message(f"요소를 찾을 수 없음: {selector}")
|
||||
|
||||
except Exception as e:
|
||||
self.log_message(f"오류 발생: {e}")
|
||||
|
||||
def find_element(self):
|
||||
"""요소 찾기 동작"""
|
||||
self.log_message("요소 찾기 시작...")
|
||||
loop = asyncio.get_event_loop()
|
||||
loop.run_until_complete(self.find_element_async())
|
||||
|
||||
async def show_current_element_info(self):
|
||||
"""현재 선택된 요소의 정보를 표시"""
|
||||
if not self.elements:
|
||||
self.log_message("요소가 없습니다.")
|
||||
return
|
||||
|
||||
current_element = self.elements[self.current_element_index]
|
||||
selector_type = self.selector_type_combobox.currentText()
|
||||
|
||||
# 요소 정보 출력
|
||||
if selector_type == "CSS":
|
||||
element_info = await current_element.evaluate("e => e.outerHTML")
|
||||
self.log_message(f"요소 {self.current_element_index + 1}/{len(self.elements)} 정보: {element_info}")
|
||||
elif selector_type == "XPath":
|
||||
element_info = await current_element.evaluate("e => e.outerHTML")
|
||||
self.log_message(f"요소 {self.current_element_index + 1}/{len(self.elements)} 정보: {element_info}")
|
||||
elif selector_type == "Name":
|
||||
element_info = await current_element.evaluate("e => e.outerHTML")
|
||||
self.log_message(f"요소 {self.current_element_index + 1}/{len(self.elements)} 정보: {element_info}")
|
||||
|
||||
def show_next_element(self):
|
||||
"""다음 요소 확인"""
|
||||
if not self.elements:
|
||||
self.log_message("찾은 요소가 없습니다.")
|
||||
return
|
||||
|
||||
self.current_element_index = (self.current_element_index + 1) % len(self.elements)
|
||||
loop = asyncio.get_event_loop()
|
||||
loop.run_until_complete(self.show_current_element_info())
|
||||
|
||||
async def click_element_async(self):
|
||||
"""요소 클릭 비동기 작업"""
|
||||
if not self.elements:
|
||||
self.log_message("요소가 없어서 클릭할 수 없습니다.")
|
||||
return
|
||||
|
||||
try:
|
||||
current_element = self.elements[self.current_element_index]
|
||||
await current_element.click()
|
||||
self.log_message(f"요소 클릭 성공: {self.current_element_index + 1}/{len(self.elements)}")
|
||||
|
||||
except Exception as e:
|
||||
self.log_message(f"클릭 오류 발생: {e}")
|
||||
|
||||
def click_element(self):
|
||||
"""요소 클릭 동작"""
|
||||
self.log_message("요소 클릭 시작...")
|
||||
loop = asyncio.get_event_loop()
|
||||
loop.run_until_complete(self.click_element_async())
|
||||
|
||||
def copy_selector(self):
|
||||
"""현재 선택한 요소의 선택자를 클립보드로 복사"""
|
||||
if not self.elements:
|
||||
self.log_message("복사할 요소가 없습니다.")
|
||||
return
|
||||
|
||||
selector = self.selector_input.text()
|
||||
self.clipboard.setText(selector)
|
||||
self.log_message(f"선택자 복사 완료: {selector}")
|
||||
|
||||
if __name__ == '__main__':
|
||||
app = QApplication(sys.argv)
|
||||
ex = PlaywrightTester()
|
||||
ex.show()
|
||||
sys.exit(app.exec_())
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
# vertexAI.py 파일을 import 후 사용
|
||||
from vertexAI import VertexAITranslator
|
||||
import json
|
||||
|
||||
# key_path 설정
|
||||
key_path = 'leensoo1nt.json'
|
||||
|
||||
# VertexAITranslator 인스턴스 생성
|
||||
translator = VertexAITranslator(key_path)
|
||||
|
||||
# 원본 옵션 데이터 예시
|
||||
original_data = {
|
||||
"origin_product_title": "多乐信工业冷风机DAKC-27B移动空调一体机车间岗位设备厨房冷气机",
|
||||
"origin_option_1": "单管制冷大一匹DAKC-27B",
|
||||
"origin_option_2": "双管制冷1.5匹DAKC-35A",
|
||||
"origin_option_3": "三管制冷大三匹DAKC-65",
|
||||
"origin_option_4": "单管制冷小一匹DAKC-18",
|
||||
"origin_option_5": "DAKC-82",
|
||||
"origin_option_6": "DAKC-130",
|
||||
"origin_option_7": "DAKC-250"
|
||||
}
|
||||
|
||||
# 옵션 번역 메서드 호출
|
||||
translated_options = translator.translate_options(original_data)
|
||||
|
||||
# 결과 출력
|
||||
print(json.dumps(translated_options, ensure_ascii=False, indent=2))
|
||||
|
|
@ -0,0 +1,115 @@
|
|||
import os
|
||||
import json
|
||||
from vertexai.generative_models import GenerativeModel
|
||||
|
||||
class VertexAITranslator:
|
||||
def __init__(self, logger, key_path):
|
||||
"""
|
||||
VertexAITranslator 클래스 초기화 메서드.
|
||||
|
||||
:param logger: 로깅을 위한 로거 객체.
|
||||
:param key_path: Google Application Credentials의 파일 경로.
|
||||
"""
|
||||
self.logger = logger
|
||||
|
||||
# GOOGLE_APPLICATION_CREDENTIALS 환경 변수 설정
|
||||
self.logger.debug(f"GOOGLE_APPLICATION_CREDENTIALS 환경 변수를 설정: {key_path}")
|
||||
os.environ['GOOGLE_APPLICATION_CREDENTIALS'] = key_path
|
||||
|
||||
# GenerativeModel 객체 초기화
|
||||
self.logger.debug("Vertex AI 모델 초기화 중...")
|
||||
self.model = GenerativeModel("gemini-1.5-flash-001")
|
||||
|
||||
# prompt.json 파일 불러오기
|
||||
self.logger.debug("prompt.json 파일을 불러옵니다.")
|
||||
self.prompt_data = self.load_prompt()
|
||||
|
||||
def load_prompt(self):
|
||||
"""
|
||||
prompt.json 파일을 읽어와 파싱하는 메서드.
|
||||
|
||||
:return: 파싱된 JSON 데이터.
|
||||
"""
|
||||
try:
|
||||
prompt_path = os.path.join(os.path.dirname(__file__), 'prompt.json')
|
||||
self.logger.debug(f"프롬프트 파일 경로: {prompt_path}")
|
||||
with open(prompt_path, 'r', encoding='utf-8') as file:
|
||||
prompt_data = json.load(file)
|
||||
self.logger.debug("prompt.json 파일이 성공적으로 로드되었습니다.")
|
||||
return prompt_data
|
||||
except FileNotFoundError as e:
|
||||
self.logger.error(f"prompt.json 파일을 찾을 수 없습니다: {e}")
|
||||
raise e
|
||||
except json.JSONDecodeError as e:
|
||||
self.logger.error(f"prompt.json 파일 파싱 중 오류 발생: {e}")
|
||||
raise e
|
||||
|
||||
def clean_special_chars(self, text):
|
||||
"""
|
||||
텍스트에서 허용되지 않는 특수 문자를 제거하고,
|
||||
필요한 특수 문자를 대체하는 메서드.
|
||||
|
||||
:param text: 입력 텍스트.
|
||||
:return: 정리된 텍스트.
|
||||
"""
|
||||
self.logger.debug(f"텍스트에서 특수 문자를 정리 중: {text}")
|
||||
|
||||
allowed_chars = self.prompt_data['allowed_special_chars']
|
||||
replacements = self.prompt_data['special_char_replacements']
|
||||
cleaned_text = []
|
||||
|
||||
for char in text:
|
||||
if char in replacements:
|
||||
cleaned_text.append(replacements[char]) # 대체 문자 추가
|
||||
self.logger.debug(f"문자 '{char}'를 대체 문자로 변경: {replacements[char]}")
|
||||
elif char not in allowed_chars and not char.isalnum() and not char.isspace():
|
||||
self.logger.debug(f"허용되지 않은 문자 제거: {char}")
|
||||
continue # 특수 문자 제거
|
||||
else:
|
||||
cleaned_text.append(char) # 허용된 문자 추가
|
||||
self.logger.debug(f"허용된 문자 추가: {char}")
|
||||
|
||||
cleaned_text_str = ''.join(cleaned_text)
|
||||
self.logger.debug(f"정리된 텍스트: {cleaned_text_str}")
|
||||
return cleaned_text_str
|
||||
|
||||
def translate_options(self, original_data):
|
||||
"""
|
||||
주어진 옵션 데이터를 Vertex AI 모델을 통해 번역하는 메서드.
|
||||
|
||||
:param original_data: 원본 옵션 데이터 (dict 형태).
|
||||
:return: 번역된 옵션명 (파이썬의 dict 형태).
|
||||
"""
|
||||
self.logger.debug(f"옵션 데이터를 번역 중: {original_data}")
|
||||
|
||||
# 데이터 정리
|
||||
cleaned_data = {key: self.clean_special_chars(value) for key, value in original_data.items()}
|
||||
self.logger.debug(f"정리된 옵션 데이터: {cleaned_data}")
|
||||
|
||||
# 원본 데이터를 프롬프트 템플릿에 넣는다.
|
||||
option_prompt_template = self.prompt_data['option_prompt_template']
|
||||
prompt = option_prompt_template.format(options=json.dumps(cleaned_data, ensure_ascii=False))
|
||||
self.logger.debug(f"생성된 프롬프트: {prompt}")
|
||||
|
||||
# Vertex AI 모델에 프롬프트 전달하여 응답 받기
|
||||
self.logger.debug("Vertex AI 모델에 프롬프트를 전달하여 응답을 기다리는 중...")
|
||||
response = self.model.generate_content(prompt)
|
||||
self.logger.debug(f"모델 응답: {response.text}")
|
||||
|
||||
# 응답 데이터에서 JSON 형식 추출
|
||||
start = response.text.find('{')
|
||||
end = response.text.rfind('}') + 1
|
||||
if start != -1 and end != -1:
|
||||
json_text = response.text[start:end]
|
||||
self.logger.debug(f"응답에서 추출된 JSON 텍스트: {json_text}")
|
||||
try:
|
||||
translated_data = json.loads(json_text)
|
||||
self.logger.debug(f"번역된 데이터: {translated_data}")
|
||||
except json.JSONDecodeError as e:
|
||||
self.logger.error(f"응답 데이터를 파싱하는 중 오류 발생: {e}")
|
||||
raise ValueError(f"응답 데이터를 파이썬의 딕셔너리로 파싱하는 중 오류 발생: {e}")
|
||||
else:
|
||||
self.logger.error("응답 데이터에서 유효한 JSON 형식을 찾을 수 없습니다.")
|
||||
raise ValueError("응답 데이터에서 유효한 JSON 형식을 찾을 수 없습니다.")
|
||||
|
||||
return translated_data
|
||||
|
|
@ -6,13 +6,13 @@ from pyvda import VirtualDesktop, get_virtual_desktops
|
|||
import subprocess
|
||||
|
||||
class WhaleTranslator:
|
||||
def __init__(self, app, logger, debug_mode=False):
|
||||
def __init__(self, app, logger, secret_mode=True, vd_mode=False):
|
||||
self.app = app
|
||||
self.logger = logger
|
||||
self.debug_mode = debug_mode
|
||||
self.vd_mode = vd_mode
|
||||
self.newtab = "about:newtab"
|
||||
|
||||
isSecret = True
|
||||
isSecret = secret_mode
|
||||
|
||||
if isSecret:
|
||||
self.whale_window_name = "새 시크릿 탭 - Whale"
|
||||
|
|
@ -21,16 +21,20 @@ class WhaleTranslator:
|
|||
|
||||
self.whale_hwnd = None
|
||||
|
||||
self.ensure_virtual_desktop_2_exists()
|
||||
if self.vd_mode:
|
||||
self.ensure_virtual_desktop_2_exists()
|
||||
|
||||
self.start_whale_browser()
|
||||
self.return_to_virtual_desktop_1()
|
||||
|
||||
|
||||
if self.vd_mode:
|
||||
self.return_to_virtual_desktop_1()
|
||||
|
||||
def start_whale_browser(self):
|
||||
# Whale 브라우저 실행
|
||||
whale_path = r"C:\\Program Files\\Naver\\Naver Whale\\Application\\whale.exe" # whale.exe 경로 지정
|
||||
# 웨일을 시크릿 모드로 실행
|
||||
subprocess.Popen([whale_path, '--incognito'])
|
||||
self.find_whale_window()
|
||||
|
||||
pyautogui.hotkey('ctrl', 'l') # 혹은 'ctrl', 'shift'를 사용할 수도 있음
|
||||
|
||||
|
|
@ -85,6 +89,15 @@ class WhaleTranslator:
|
|||
except Exception as e:
|
||||
self.logger.debug(f"가상 데스크톱 확인/생성 중 오류 발생: {e}")
|
||||
|
||||
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로 전환"""
|
||||
|
|
@ -105,8 +118,8 @@ class WhaleTranslator:
|
|||
self.logger.debug(f"가상 데스크톱 전환 중 오류 발생: {e}")
|
||||
|
||||
def translate_image(self, url):
|
||||
# 가상 데스크톱 2에서 웨일 작업 수행
|
||||
self.switch_to_virtual_desktop_2()
|
||||
if self.vd_mode:
|
||||
self.switch_to_virtual_desktop_2()
|
||||
|
||||
if self.find_whale_window():
|
||||
win32gui.ShowWindow(self.whale_hwnd, win32con.SW_RESTORE) # 웨일 창 활성화
|
||||
|
|
@ -135,7 +148,9 @@ class WhaleTranslator:
|
|||
pyautogui.typewrite(self.newtab) # URL을 입력
|
||||
self.enter_url(self.newtab)
|
||||
self.logger.debug(f'번역 완료: {url}')
|
||||
self.return_to_virtual_desktop_1()
|
||||
|
||||
if self.vd_mode:
|
||||
self.return_to_virtual_desktop_1()
|
||||
else:
|
||||
self.logger.debug('웨일 창을 찾을 수 없습니다.')
|
||||
|
||||
|
|
@ -188,7 +203,7 @@ class WhaleTranslator:
|
|||
"""'새 시크릿 탭 - Whale' 창이 존재하면 종료"""
|
||||
whale_window_name = "새 탭 - Whale"
|
||||
sec_whale_window_name = "새 시크릿 탭 - Whale"
|
||||
|
||||
|
||||
whale_hwnd = self.find_window_by_title(whale_window_name)
|
||||
sec_whale_hwnd = self.find_window_by_title(sec_whale_window_name)
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,246 @@
|
|||
이미지 번역 외 추가기능을 구현하려 해.
|
||||
|
||||
아래 추가정보를 참고해서만들어줘.
|
||||
|
||||
1. 단일옵션인지 아닌지를 먼저 판단해.
|
||||
|
||||
2. 단일옵션이 아닐경우 전체옵션체크박스의 상태를 확인해.
|
||||
전체옵션 체크박스가 일부만 체크되어 있다면 해당상품은 옵션수정이 완료된 상품이기 때문에 옵션수정을 pass해야해.
|
||||
|
||||
3. 가격 낮은 순 정렬버튼을 1회 클릭
|
||||
|
||||
4. 단일 옵션이 아니면서, 전체 옵션체크박스가 전체체크가 되어 있다면 아래의 정보들을 참고해서 옵션정보를 수집해서 옵션 info 딕셔너리를 만들어.
|
||||
총 옵션갯수, 각 옵션의 원본옵션명, 각옵션의 가격범위, 옵션이미지 존재 여부 등
|
||||
|
||||
5.수집된 옵션명을 vertexAI객체의 translate_options 메서드를 이용해 번역된 옵션값을 딕셔너리로 리턴받아와서 옵션info에 업데이트 해줘.
|
||||
|
||||
6. 번역된 옵션명을 각각의 옵션명 편집칸에 입력해줘.
|
||||
|
||||
7. 옵션이 3개 이상이면 가장 낮은 옵션을 1개 체크해제, 2개 이하라면 이 과정을 수행하지 않아.
|
||||
|
||||
8. 선택된 총 옵션갯수가 10개이상이 되지 않도록 10개 이상이면 나머지 옵션들을 체크해제해야해.
|
||||
|
||||
9. 옵션편집이 완료되면 저장 버튼을 클릭해야해.
|
||||
|
||||
|
||||
|
||||
|
||||
전체 옵션체크박스의 요소에서 모든옵션이 체크된 상태일 경우 총 옵션의 갯수를 가져올수 있어.
|
||||
|
||||
가격 낮은 순 버튼 요소
|
||||
<button type="button" class="ant-btn css-1li46mu ant-btn-default"><span>가격 낮은 순 </span><span role="img" aria-label="arrow-down" class="anticon anticon-arrow-down"><svg viewBox="64 64 896 896" focusable="false" data-icon="arrow-down" width="1em" height="1em" fill="currentColor" aria-hidden="true"><path d="M862 465.3h-81c-4.6 0-9 2-12.1 5.5L550 723.1V160c0-4.4-3.6-8-8-8h-60c-4.4 0-8 3.6-8 8v563.1L255.1 470.8c-3-3.5-7.4-5.5-12.1-5.5h-81c-6.8 0-10.5 8.1-6 13.2L487.9 861a31.96 31.96 0 0048.3 0L868 478.5c4.5-5.2.8-13.2-6-13.2z"></path></svg></span></button>
|
||||
가격 낮은 순 버튼 css
|
||||
#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-bklklh.iBKTwf > div.ant-row.ant-row-space-between.ant-row-middle.css-1li46mu > div:nth-child(2) > div > button:nth-child(3)
|
||||
|
||||
A-Z버튼 요소
|
||||
<button type="button" class="ant-btn css-1li46mu ant-btn-default"><span>A-Z</span></button>
|
||||
A-Z버튼 css
|
||||
#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-bklklh.iBKTwf > div:nth-child(1) > div:nth-child(1) > div > div:nth-child(1) > button
|
||||
|
||||
전체 체크박스의 css
|
||||
#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-bklklh.iBKTwf > div.ant-row.ant-row-space-between.ant-row-middle.css-1li46mu > div:nth-child(1) > label > span.ant-checkbox.ant-checkbox-indeterminate.ant-wave-target.css-1li46mu > input
|
||||
|
||||
전체 체크박스가 모든옵션이 선택된 상태일 경우 요소
|
||||
<label class="ant-checkbox-wrapper ant-checkbox-wrapper-checked css-1li46mu"><span class="ant-checkbox ant-wave-target css-1li46mu ant-checkbox-checked"><input class="ant-checkbox-input" type="checkbox"><span class="ant-checkbox-inner"></span></span><span>선택 8개 옵션명</span></label>
|
||||
전체 체크박스가 모든옵션이 선택된 상태일 경우 css
|
||||
#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-bklklh.iBKTwf > div.ant-row.ant-row-space-between.ant-row-middle.css-1li46mu > div:nth-child(1) > label
|
||||
|
||||
전체 체크박스가 일부옵션이 선택된 상태일 경우 요소
|
||||
<label class="ant-checkbox-wrapper css-1li46mu"><span class="ant-checkbox ant-checkbox-indeterminate ant-wave-target css-1li46mu"><input class="ant-checkbox-input" type="checkbox" aria-checked="mixed"><span class="ant-checkbox-inner"></span></span><span>선택 6개 옵션명</span></label>
|
||||
전체 체크박스가 일부옵션이 선택된 상태일 경우 css
|
||||
#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-bklklh.iBKTwf > div.ant-row.ant-row-space-between.ant-row-middle.css-1li46mu > div:nth-child(1) > label
|
||||
|
||||
전체 체크박스가 모든옵션이 선택되지 않은 상태일 경우 요소
|
||||
<label class="ant-checkbox-wrapper css-1li46mu"><span class="ant-checkbox ant-checkbox-indeterminate ant-wave-target css-1li46mu"><input class="ant-checkbox-input" type="checkbox" aria-checked="mixed"><span class="ant-checkbox-inner"></span></span><span>선택 6개 옵션명</span></label>
|
||||
전체 체크박스가 모든옵션이 선택되지 않은 상태일 경우 css
|
||||
#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-bklklh.iBKTwf > div.ant-row.ant-row-space-between.ant-row-middle.css-1li46mu > div:nth-child(1) > label
|
||||
|
||||
|
||||
1번옵션 체크박스 css
|
||||
#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(1) > div > div:nth-child(1) > div > div:nth-child(1) > label > span > input
|
||||
1번옵션의 원본옵션명 요소
|
||||
<span class="sc-drFUgV dIaFuk Body3Regular14 CharacterSecondary45">8吨力机器整套+一字刀头 送脚踏开关</span>
|
||||
1번옵션의 원본옵션명 css
|
||||
#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(1) > div > div:nth-child(1) > div > div:nth-child(3) > div:nth-child(3) > span
|
||||
1번옵션의 옵션명편집칸 요소
|
||||
<input class="ant-input css-1li46mu" type="text" value="힘 기계 완전한 세트 + 풋 스위치를 보내는 배열된 절단기 머리의 8 톤">
|
||||
1번옵션의 옵션명편집칸 css
|
||||
#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(1) > div > div:nth-child(1) > div > div:nth-child(3) > div:nth-child(2) > div:nth-child(1) > span > input
|
||||
1번옵션의 가격범위 요소
|
||||
<sup data-show="true" class="ant-scroll-number ant-badge-count ant-badge-multiple-words" title="534,100원 - 534,100원">534,100원 - 534,100원</sup>
|
||||
1번옵션의 가격범위 css
|
||||
#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(1) > div > div:nth-child(1) > div > div:nth-child(3) > div:nth-child(1) > div:nth-child(2) > button > span > sup
|
||||
|
||||
|
||||
2번옵션 체크박스 css
|
||||
#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(2) > div > div:nth-child(1) > div > div:nth-child(1) > label > span > input
|
||||
2번옵션의 원본옵션명 요소
|
||||
<span class="sc-drFUgV dIaFuk Body3Regular14 CharacterSecondary45">8吨力机器整套+一字刀头+十字刀头 送脚踏开关</span>
|
||||
2번옵션의 원본옵션명 css
|
||||
#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(2) > div > div:nth-child(1) > div > div:nth-child(3) > div:nth-child(3) > span
|
||||
2번옵션의 옵션명편집칸 요소
|
||||
#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(2) > div > div:nth-child(1) > div > div:nth-child(3) > div:nth-child(2) > div:nth-child(1) > span > input
|
||||
2번옵션의 옵션명편집칸 css
|
||||
#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(2) > div > div:nth-child(1) > div > div:nth-child(3) > div:nth-child(2) > div:nth-child(1) > span > input
|
||||
2번옵션의 가격범위 요소
|
||||
<sup data-show="true" class="ant-scroll-number ant-badge-count ant-badge-multiple-words" title="559,000원 - 559,000원">559,000원 - 559,000원</sup>
|
||||
2번옵션의 가격범위 css
|
||||
#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(2) > div > div:nth-child(1) > div > div:nth-child(3) > div:nth-child(1) > div:nth-child(2) > button > span > sup
|
||||
|
||||
3번옵션 체크박스 css
|
||||
#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(3) > div > div:nth-child(1) > div > div:nth-child(1) > label > span > input
|
||||
|
||||
3번옵션의 원본옵션명 요소
|
||||
<span class="sc-drFUgV dIaFuk Body3Regular14 CharacterSecondary45">8吨力机器整套+一字刀头+加强脚踏开关</span>
|
||||
3번옵션의 원본옵션명 css
|
||||
#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(3) > div > div:nth-child(1) > div > div:nth-child(3) > div:nth-child(3) > span
|
||||
3번옵션의 옵션명편집칸 요소
|
||||
<input class="ant-input css-1li46mu" type="text" value="힘 기계 완전한 세트 + 배열된 절단기 머리 + 강화된 발 스위치의 8 톤">
|
||||
3번옵션의 옵션명편집칸 css
|
||||
#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(3) > div > div:nth-child(1) > div > div:nth-child(3) > div:nth-child(2) > div:nth-child(1) > span > input
|
||||
3번옵션의 가격범위 요소
|
||||
<sup data-show="true" class="ant-scroll-number ant-badge-count ant-badge-multiple-words" title="584,000원 - 584,000원">584,000원 - 584,000원</sup>
|
||||
3번옵션의 가격범위 css
|
||||
#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(3) > div > div:nth-child(1) > div > div:nth-child(3) > div:nth-child(1) > div:nth-child(2) > button > span > sup
|
||||
|
||||
|
||||
|
||||
1번옵션의 옵션이미지가 존재할 경우 옵션이미지 삭제버튼 요소
|
||||
<div class="sc-igZIGL kQDmyq"><span class="sc-drFUgV dIaFuk FootnoteDescription ">삭제</span></div>
|
||||
1번옵션의 옵션이미지가 존재할 경우 옵션이미지 삭제버튼 css
|
||||
#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(1) > 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
|
||||
|
||||
1번옵션의 옵션이미지가 없을 경우 옵션이미지 추가버튼 요소
|
||||
<img draggable="false" src="./ic_image_add.svg" class="sc-dRGYJT hmQUGb">
|
||||
1번옵션의 옵션이미지가 없을 경우 옵션이미지 추가버튼 css
|
||||
#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(1) > div > div:nth-child(1) > div > div:nth-child(2) > div > div > img
|
||||
|
||||
1번옵션의 옵션이미지가 존재할 경우 옵션이미지의 요소
|
||||
<img src="https://file.percenty.co.kr/public/652bed8e865b1f32ea62bf1f/products/66f37ebe73994c46d385c454/8c3413f1-7aee-4238-9e68-195b9e3a582f.jpg" class="sc-gbvfcU ezktkd">
|
||||
1번옵션의 옵션이미지가 존재할 경우 옵션이미지의 css
|
||||
#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(1) > div > div:nth-child(1) > div > div:nth-child(2) > div > img
|
||||
|
||||
2번옵션의 옵션이미지가 존재할 경우 옵션이미지 삭제버튼 요소
|
||||
<div class="sc-igZIGL kQDmyq"><span class="sc-drFUgV dIaFuk FootnoteDescription ">삭제</span></div>
|
||||
2번옵션의 옵션이미지가 존재할 경우 옵션이미지 삭제버튼 css
|
||||
#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(2) > 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
|
||||
|
||||
2번옵션의 옵션이미지가 없을 경우 옵션이미지 추가버튼 요소
|
||||
<img draggable="false" src="./ic_image_add.svg" class="sc-dRGYJT hmQUGb">
|
||||
2번옵션의 옵션이미지가 없을 경우 옵션이미지 추가버튼 css
|
||||
#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(2) > div > div:nth-child(1) > div > div:nth-child(2) > div > div > img
|
||||
|
||||
2번옵션의 옵션이미지가 존재할 경우 옵션이미지의 요소
|
||||
<img src="https://file.percenty.co.kr/public/652bed8e865b1f32ea62bf1f/products/66f37ebe73994c46d385c454/a68eff8d-4f66-40f7-80ef-775babb9b46c.jpg" class="sc-gbvfcU ezktkd">
|
||||
2번옵션의 옵션이미지가 존재할 경우 옵션이미지의 css
|
||||
#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(2) > div > div:nth-child(1) > div > div:nth-child(2) > div > img
|
||||
|
||||
|
||||
옵션이미지 삭제시 삭제 확인버튼 요소
|
||||
<button type="button" class="ant-btn css-1li46mu ant-btn-primary ant-btn-dangerous"><span>삭제</span></button>
|
||||
옵션이미지 삭제시 삭제 확인버튼 css
|
||||
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
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
[단일상품 여부를 판단하는 셀레니움 코드]
|
||||
def is_option_product(driver):
|
||||
"""
|
||||
웹페이지에서 현재 상품이 '단일 상품'인지 '옵션 상품'인지 확인합니다.
|
||||
|
||||
Parameters:
|
||||
- driver: WebDriver 인스턴스
|
||||
|
||||
Returns:
|
||||
- bool: 옵션 상품이면 True, 단일 상품이면 False
|
||||
"""
|
||||
try:
|
||||
# '옵션 상품등록' 또는 '단일 상품등록' 선택 여부 확인
|
||||
radio_group_css = "div#productMainContentContainerId div.ant-row.css-1li46mu > div"
|
||||
radio_group_element = WebDriverWait(driver, 3).until(
|
||||
EC.presence_of_element_located((By.CSS_SELECTOR, radio_group_css))
|
||||
)
|
||||
|
||||
# 라디오 버튼의 선택 상태 확인
|
||||
option_radio_xpath = "//label[span[text()='옵션 상품등록']]//input[@type='radio' and @value='false']"
|
||||
single_radio_xpath = "//label[span[text()='단일 상품등록']]//input[@type='radio' and @value='true']"
|
||||
|
||||
option_radio_checked = radio_group_element.find_element(By.XPATH, option_radio_xpath).is_selected()
|
||||
single_radio_checked = radio_group_element.find_element(By.XPATH, single_radio_xpath).is_selected()
|
||||
|
||||
if option_radio_checked and not single_radio_checked:
|
||||
logger.debug("상품 유형: 다양한 옵션 상품")
|
||||
return False
|
||||
else:
|
||||
logger.debug("상품 유형: 단일 상품")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"상품 유형 확인 중 오류 발생: {e}", exc_info=True)
|
||||
logger.debug("상품 유형 오류 발생으로 기본설정인 단일 상품등록으로 진행합니다.")
|
||||
return False # 오류 발생 시 기본적으로 단일 상품으로 처리
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
[vertexAI.py]
|
||||
def translate_options(self, original_data):
|
||||
"""
|
||||
옵션 데이터를 번역하고 파이썬의 딕셔너리로 반환한다.
|
||||
|
||||
:param original_data: 원본 옵션 데이터 (dict 형태)
|
||||
:return: 번역된 옵션명 (파이썬의 dict 형태)
|
||||
"""
|
||||
|
||||
# 데이터 정리
|
||||
cleaned_data = {key: self.clean_special_chars(value) for key, value in original_data.items()}
|
||||
|
||||
# 원본 데이터를 프롬프트 템플릿에 넣는다.
|
||||
option_prompt_template = self.prompt_data['option_prompt_template']
|
||||
prompt = option_prompt_template.format(options=json.dumps(cleaned_data, ensure_ascii=False))
|
||||
|
||||
# Vertex AI 모델에 프롬프트 전달하여 응답 받기
|
||||
response = self.model.generate_content(prompt)
|
||||
|
||||
# 중괄호 내부의 내용만 추출
|
||||
start = response.text.find('{')
|
||||
end = response.text.rfind('}') + 1
|
||||
if start != -1 and end != -1:
|
||||
json_text = response.text[start:end]
|
||||
try:
|
||||
translated_data = json.loads(json_text)
|
||||
except json.JSONDecodeError as e:
|
||||
raise ValueError(f"응답 데이터를 파이썬의 딕셔너리로 파싱하는 중 오류 발생: {e}")
|
||||
else:
|
||||
raise ValueError("응답 데이터에서 유효한 JSON 형식을 찾을 수 없습니다.")
|
||||
|
||||
return translated_data
|
||||
|
||||
|
||||
|
||||
[translate_options 의 original_data 형식]
|
||||
original_data = {
|
||||
"origin_option_1": "单管制冷大一匹DAKC-27B",
|
||||
"origin_option_2": "双管制冷1.5匹DAKC-35A",
|
||||
"origin_option_3": "三管制冷大三匹DAKC-65",
|
||||
"origin_option_4": "单管制冷小一匹DAKC-18",
|
||||
"origin_option_5": "DAKC-82",
|
||||
"origin_option_6": "DAKC-130",
|
||||
"origin_option_7": "DAKC-250"
|
||||
}
|
||||
|
||||
|
||||
[translate_options 의 return 형식]
|
||||
{
|
||||
"trans_option_1": "단관 1.0HP",
|
||||
"trans_option_2": "쌍관 1.5HP",
|
||||
"trans_option_3": "삼관 3.0HP",
|
||||
"trans_option_4": "단관 0.5HP",
|
||||
"trans_option_5": "DAKC-82",
|
||||
"trans_option_6": "DAKC-130",
|
||||
"trans_option_7": "DAKC-250"
|
||||
}
|
||||
Loading…
Reference in New Issue