This commit is contained in:
R5600U_PC 2024-09-25 16:31:45 +09:00
parent 5f0e48ef56
commit d52b0b0f37
19 changed files with 1799 additions and 17019 deletions

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

File diff suppressed because it is too large Load Diff

View File

@ -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' 데이터가 없습니다.")

View File

@ -4,6 +4,7 @@ import pyautogui
import time import time
import win32gui, win32con import win32gui, win32con
from bs4 import BeautifulSoup from bs4 import BeautifulSoup
import pyperclip
class BrowserController: class BrowserController:
def __init__(self, app, logger): def __init__(self, app, logger):
@ -18,9 +19,6 @@ class BrowserController:
self.browser = None self.browser = None
self.page = None self.page = None
def get_page(self):
return self.page
def start_browser(self): def start_browser(self):
"""크롬 브라우저 실행 및 페이지 로딩""" """크롬 브라우저 실행 및 페이지 로딩"""
self.logger.debug('크롬 브라우저 실행 중...') self.logger.debug('크롬 브라우저 실행 중...')
@ -41,7 +39,6 @@ class BrowserController:
self.logger.debug('newPage 로딩 ...') self.logger.debug('newPage 로딩 ...')
# 사용자는 이 시점에 로그인 및 상세페이지 편집 모드로 들어감 # 사용자는 이 시점에 로그인 및 상세페이지 편집 모드로 들어감
# 페이지 제목을 가져와서 창 제목으로 활용 # 페이지 제목을 가져와서 창 제목으로 활용
page_title = self.page.title() page_title = self.page.title()
self.logger.debug(f'페이지 제목: {page_title}') self.logger.debug(f'페이지 제목: {page_title}')
@ -107,16 +104,64 @@ class BrowserController:
else: else:
self.logger.debug('크롬 창을 찾을 수 없습니다.') self.logger.debug('크롬 창을 찾을 수 없습니다.')
# def switch_to_whale(self):
# """웨일 브라우저로 포커스 전환""" def get_total_product_count(self):
# if not self.whale_hwnd: try:
# self.whale_hwnd = self.find_window_by_title(self.whale_window_name) # JavaScript로 해당 요소의 텍스트를 가져옴
# if self.whale_hwnd: element_text = self.page.evaluate('''() => {
# win32gui.ShowWindow(self.whale_hwnd, win32con.SW_RESTORE) 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)');
# win32gui.SetForegroundWindow(self.whale_hwnd) return element ? element.innerText : null;
# self.logger.debug('웨일 창으로 포커스 이동.') }''')
# else:
# self.logger.debug('웨일 창을 찾을 수 없습니다.') 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): def extract_image_urls(self):
"""HTML에서 이미지 URL 추출 및 img 태그 삭제 후 소스 버튼 다시 클릭""" """HTML에서 이미지 URL 추출 및 img 태그 삭제 후 소스 버튼 다시 클릭"""
@ -234,9 +279,9 @@ class BrowserController:
self.scroll_page_to_bottom() self.scroll_page_to_bottom()
# 스크롤하여 모든 버튼을 화면에 표시 (가장 하단까지 스크롤) # # 스크롤하여 모든 버튼을 화면에 표시 (가장 하단까지 스크롤)
self.page.evaluate("""window.scrollTo(0, document.body.scrollHeight);""") # self.page.evaluate("""window.scrollTo(0, document.body.scrollHeight);""")
self.logger.debug("페이지를 아래로 스크롤했습니다.") # self.logger.debug("페이지를 아래로 스크롤했습니다.")
# 버튼 선택 (확실한 선택자를 사용하여 확인) # 버튼 선택 (확실한 선택자를 사용하여 확인)
buttons = self.page.locator('button:has-text("세부사항 수정 및 업로드")') buttons = self.page.locator('button:has-text("세부사항 수정 및 업로드")')
@ -271,6 +316,14 @@ class BrowserController:
except Exception as e: except Exception as e:
self.logger.debug(f"상세페이지 탭 클릭 중 오류: {str(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): def extract_image_urls(self):
"""상세페이지에서 이미지 URL 추출""" """상세페이지에서 이미지 URL 추출"""
try: try:
@ -321,14 +374,18 @@ class BrowserController:
except Exception as e: except Exception as e:
self.logger.debug(f"이미지 번역 중 오류: {str(e)}") self.logger.debug(f"이미지 번역 중 오류: {str(e)}")
def paste_image_in_chrome(self, base64toimage, url): def paste_image_in_chrome(self, clipboardImageManager, url):
"""크롬으로 포커스를 옮기고 클립보드의 이미지를 붙여넣고 엔터 입력""" """크롬으로 포커스를 옮기고 클립보드의 이미지를 붙여넣고 엔터 입력"""
try: try:
self.switch_to_chrome() # 크롬으로 포커스 이동 self.switch_to_chrome() # 크롬으로 포커스 이동
base64toimage.process_clipboard(url) # 클립보드 내용이 base64일 경우 이미지로 변환 clipboardImageManager.process_clipboard(url) # 클립보드 내용을 처리
pyautogui.hotkey('ctrl', 'v') # 클립보드 이미지 붙여넣기 clipboard_content = pyperclip.paste()
pyautogui.press('right') # 오른쪽 입력 if clipboard_content:
self.logger.debug("이미지 붙여넣기 완료.") pyautogui.hotkey('ctrl', 'v') # 클립보드 이미지 붙여넣기
pyautogui.press('right') # 오른쪽 입력
self.logger.debug("이미지 붙여넣기 완료.")
else:
self.logger.debug("클립보드가 비어있습니다.")
except Exception as e: except Exception as e:
self.logger.debug(f"이미지 붙여넣기 중 오류: {str(e)}") self.logger.debug(f"이미지 붙여넣기 중 오류: {str(e)}")
@ -344,18 +401,31 @@ class BrowserController:
def go_to_next_page(self): def go_to_next_page(self):
"""다음 페이지로 이동""" """다음 페이지로 이동"""
try: 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: if not current_page:
next_button.click() self.logger.debug("현재 페이지 정보를 찾을 수 없습니다.")
self.page.wait_for_load_state('domcontentloaded') return False
self.logger.debug("다음 페이지로 이동 완료.")
# 현재 활성화된 페이지 번호를 가져옴
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 return True
else: else:
self.logger.debug("다음 페이지가 없습니다.") self.logger.debug("다음 페이지가 없습니다.")
return False return False
except Exception as e: except Exception as e:
self.logger.debug(f"다음 페이지로 이동 중 오류: {str(e)}") self.logger.debug(f"다음 페이지로 이동 중 오류 발생: {str(e)}")
return False return False
def switch_to_chrome(self): def switch_to_chrome(self):

296
clipboardImageManager.py Normal file
View File

@ -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
View File

@ -1,9 +1,11 @@
from PyQt5.QtWidgets import QWidget, QPushButton, QVBoxLayout, QTextEdit, QLabel, QLineEdit, QHBoxLayout from PyQt5.QtWidgets import QWidget, QPushButton, QVBoxLayout, QTextEdit, QLabel, QLineEdit, QHBoxLayout, QProgressBar, QSizePolicy
from PyQt5.QtCore import Qt, QRect, QSettings from PyQt5.QtCore import Qt, QRect, QSettings, QTimer
from toggleSwitch import ToggleSwitch from toggleSwitch import ToggleSwitch
from browser_control import BrowserController from browser_control import BrowserController
from whale_translator import WhaleTranslator 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 # 추가 from logger_module import QTextEditLogger # 추가
import logging import logging
@ -12,18 +14,42 @@ class TranslationApp(QWidget):
super().__init__() super().__init__()
self.initUI() self.initUI()
self.logger = logger self.logger = logger
key_path = 'leensoo1nt.json'
self.settings = QSettings("WhenRideMycar", "TranslationApp") # QSettings 초기화 self.settings = QSettings("WhenRideMycar", "TranslationApp") # QSettings 초기화
self.browser_controller = BrowserController(self, self.logger) self.browser_controller = BrowserController(self, self.logger)
self.whale_translator = WhaleTranslator(self, self.logger, debug_mode=False) # 디버그 모드 켜기 self.whale_translator = WhaleTranslator(self, self.logger, secret_mode=True,vd_mode=True) # 디버그 모드 켜기
self.base64TOImage = base64TOImage(self, logger, self.browser_controller) 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.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() self.load_settings()
# QTextEditLogger 추가 # 로거 초기화
self.add_text_edit_logger() self.add_text_edit_logger()
# 프로그래스바 초기화
self.update_total_progress(0,0)
def add_text_edit_logger(self): def add_text_edit_logger(self):
"""QTextEdit에 로그를 출력하기 위한 핸들러 추가""" """QTextEdit에 로그를 출력하기 위한 핸들러 추가"""
text_edit_logger = QTextEditLogger() text_edit_logger = QTextEditLogger()
@ -32,7 +58,40 @@ class TranslationApp(QWidget):
text_edit_logger.appendHtml.connect(self.log.append) # appendHtml 대신 append로 수정 text_edit_logger.appendHtml.connect(self.log.append) # appendHtml 대신 append로 수정
text_edit_logger.scrollToBottom.connect(lambda: self.log.verticalScrollBar().setValue(self.log.verticalScrollBar().maximum())) text_edit_logger.scrollToBottom.connect(lambda: self.log.verticalScrollBar().setValue(self.log.verticalScrollBar().maximum()))
self.logger.addHandler(text_edit_logger) 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): def initUI(self):
self.setWindowFlags(Qt.WindowStaysOnTopHint) self.setWindowFlags(Qt.WindowStaysOnTopHint)
@ -43,6 +102,31 @@ class TranslationApp(QWidget):
self.log = QTextEdit(self) self.log = QTextEdit(self)
self.log.setReadOnly(True) 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 = ToggleSwitch(self)
self.admin_toggle.clicked.connect(self.on_toggle_clicked) self.admin_toggle.clicked.connect(self.on_toggle_clicked)
@ -50,6 +134,8 @@ class TranslationApp(QWidget):
# 관리자 ID 및 PW # 관리자 ID 및 PW
self.admin_id_label = QLabel("관리자 ID:", self) self.admin_id_label = QLabel("관리자 ID:", self)
self.admin_id_input = QLineEdit(self) self.admin_id_input = QLineEdit(self)
# 관리자 PW
self.admin_pw_label = QLabel("관리자 PW:", self) self.admin_pw_label = QLabel("관리자 PW:", self)
self.admin_pw_input = QLineEdit(self) self.admin_pw_input = QLineEdit(self)
self.admin_pw_input.setEchoMode(QLineEdit.Password) self.admin_pw_input.setEchoMode(QLineEdit.Password)
@ -67,36 +153,61 @@ class TranslationApp(QWidget):
self.pause_button = QPushButton('일시정지', self) self.pause_button = QPushButton('일시정지', self)
self.exit_button = QPushButton('종료', self) self.exit_button = QPushButton('종료', self)
# 레이아웃 설정 # 버튼 크기를 1.5배로 설정
layout = QVBoxLayout() button_height = int(self.start_chrome_button.sizeHint().height() * 1.5)
self.start_chrome_button.setFixedHeight(button_height)
# 관리자 토글 버튼 및 로그인 관련 필드 추가 self.translate_button.setFixedHeight(button_height)
toggle_layout = QHBoxLayout() self.pause_button.setFixedHeight(button_height)
toggle_layout.addWidget(QLabel("관리자 여부:", self)) self.exit_button.setFixedHeight(button_height)
toggle_layout.addWidget(self.admin_toggle)
layout.addLayout(toggle_layout)
# 관리자 ID/PW # 메인 레이아웃 설정
layout.addWidget(self.admin_id_label) self.main_layout = QVBoxLayout()
layout.addWidget(self.admin_id_input)
layout.addWidget(self.admin_pw_label) # 관리자 토글 버튼 및 로그인 관련 필드 추가
layout.addWidget(self.admin_pw_input) 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 # 직원 ID/PW
layout.addWidget(self.user_id_label) self.user_layout = QVBoxLayout()
layout.addWidget(self.user_id_input) self.user_layout.addWidget(self.user_id_label)
layout.addWidget(self.user_pw_label) self.user_layout.addWidget(self.user_id_input)
layout.addWidget(self.user_pw_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) self.button_layout = QHBoxLayout()
layout.addWidget(self.translate_button) self.button_layout.addWidget(self.start_chrome_button)
layout.addWidget(self.pause_button) self.button_layout.addWidget(self.translate_button)
layout.addWidget(self.exit_button) self.button_layout.addWidget(self.pause_button)
self.setLayout(layout) 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) self.on_toggle_clicked(False)
@ -108,27 +219,23 @@ class TranslationApp(QWidget):
self.exit_button.clicked.connect(self.close) self.exit_button.clicked.connect(self.close)
def on_toggle_clicked(self, is_checked): def on_toggle_clicked(self, is_checked):
"""관리자 토글 상태에 따라 필드 활성화/비활성화""" """관리자 토글 상태에 따라 관리자와 직원 필드를 표시/숨김"""
if is_checked: if is_checked:
# 관리자 모드 # 관리자 모드: 직원 레이아웃을 숨기고, 관리자 PW를 표시
# self.admin_id_label.setVisible(True) self.set_layout_visibility(self.admin_layout, True)
# self.admin_id_input.setVisible(True) self.set_layout_visibility(self.user_layout, False)
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)
else: else:
# 직원 모드 # 직원 모드: 관리자 PW를 숨기고, 직원 레이아웃을 표시
# self.admin_id_label.setVisible(False) self.set_layout_visibility(self.admin_layout, False)
# self.admin_id_input.setVisible(False) self.set_layout_visibility(self.user_layout, True)
self.admin_pw_label.setVisible(False)
self.admin_pw_input.setVisible(False) def set_layout_visibility(self, changelayout, visible):
self.user_id_label.setVisible(True) """레이아웃에 포함된 모든 위젯의 가시성을 설정"""
self.user_id_input.setVisible(True) for i in range(changelayout.count()):
self.user_pw_label.setVisible(True) widget = changelayout.itemAt(i).widget()
self.user_pw_input.setVisible(True) if widget:
widget.setVisible(visible)
def start_browser(self): def start_browser(self):
"""크롬 브라우저 실행 후 로그인""" """크롬 브라우저 실행 후 로그인"""
@ -171,23 +278,48 @@ class TranslationApp(QWidget):
self.admin_toggle.setChecked(admin_toggle_state) self.admin_toggle.setChecked(admin_toggle_state)
self.on_toggle_clicked(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): def start_translation(self):
self.logger.debug('번역 작업을 시작합니다...') self.logger.debug('번역 작업을 시작합니다...')
self.running = True # 번역 작업이 시작됨 self.running = True # 번역 작업이 시작됨
try: try:
# # 1. 광고 다이얼로그가 나타날 경우 닫기 처리 # 1. "신규 상품 등록" 페이지로 이동
# self.logger.debug('광고 다이얼로그 닫기 처리 중...')
# self.browser_controller.close_ad_dialog_if_present()
# 2. "신규 상품 등록" 페이지로 이동
self.logger.debug('신규 상품 등록 페이지로 이동 중...') self.logger.debug('신규 상품 등록 페이지로 이동 중...')
self.browser_controller.go_to_new_product_page() self.browser_controller.go_to_new_product_page()
# # Playwright에서 페이지 스크롤 후 "세부사항 수정 및 업로드" 버튼 수집 # 2. 총 상품 수 수집
# self.browser_controller.scroll_page_to_bottom() 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 page_number = 1
while self.running: while self.running:
self.logger.debug(f'현재 페이지: {page_number}') self.logger.debug(f'현재 페이지: {page_number}')
@ -207,28 +339,31 @@ class TranslationApp(QWidget):
self.logger.debug(f'{index}/{len(product_buttons)}: 세부사항 수정 작업 중...') 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.open_product_edit_dialog(button)
# 상세페이지 탭 클릭 # 옵션 수정
self.browser_controller.click_detail_tab() self.start_stage(0)
self.edit_option()
# 이미지 URL 추출 self.complete_stage(0)
image_urls = self.browser_controller.extract_image_urls()
# 상세페이지 수정
# 이미지 번역 작업 진행 self.start_stage(1)
for url in image_urls: self.detail_trans()
if not self.running: self.complete_stage(1)
self.logger.debug('번역 작업이 중단되었습니다.')
break
self.whale_translator.translate_image(url)
self.browser_controller.paste_image_in_chrome(self.base64TOImage, url)
# 수정 후 저장 # 수정 후 저장
self.logger.debug('상품 세부사항 저장 중...') self.logger.debug('상품 세부사항 저장 중...')
self.browser_controller.save_product_edit() self.browser_controller.save_product_edit()
completed_count += 1
self.update_total_progress(completed_count,total_products)
self.logger.debug('상품 수정 완료.') self.logger.debug('상품 수정 완료.')
# 6. 다음 페이지로 이동 (있으면) # 6. 다음 페이지로 이동 (있으면)
@ -245,30 +380,6 @@ class TranslationApp(QWidget):
self.logger.debug(f'번역 작업 중 오류 발생: {str(e)}') self.logger.debug(f'번역 작업 중 오류 발생: {str(e)}')
self.running = False 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): def pause_translation(self):
self.logger.debug('번역 작업을 중단합니다...') self.logger.debug('번역 작업을 중단합니다...')
self.running = False # 번역 작업 중단 self.running = False # 번역 작업 중단
@ -279,3 +390,53 @@ class TranslationApp(QWidget):
self.browser_controller.close_browser() # 브라우저 종료 self.browser_controller.close_browser() # 브라우저 종료
self.whale_translator.close_all_virtual_desktops() self.whale_translator.close_all_virtual_desktops()
super().close() 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)

13
leensoo1nt.json Normal file
View File

@ -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"
}

199
option.py Normal file
View File

@ -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)}")

16
prompt.json Normal file
View File

@ -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",
"【" : "(",
"】" : ")",
"[" : "(",
"]" : ")",
"," : "."
}
}

210
test/playwrightTest.py Normal file
View File

@ -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_())

27
test/vertexTest.py Normal file
View File

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

115
vertexAI.py Normal file
View File

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

View File

@ -6,13 +6,13 @@ from pyvda import VirtualDesktop, get_virtual_desktops
import subprocess import subprocess
class WhaleTranslator: 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.app = app
self.logger = logger self.logger = logger
self.debug_mode = debug_mode self.vd_mode = vd_mode
self.newtab = "about:newtab" self.newtab = "about:newtab"
isSecret = True isSecret = secret_mode
if isSecret: if isSecret:
self.whale_window_name = "새 시크릿 탭 - Whale" self.whale_window_name = "새 시크릿 탭 - Whale"
@ -21,16 +21,20 @@ class WhaleTranslator:
self.whale_hwnd = None 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.start_whale_browser()
self.return_to_virtual_desktop_1()
if self.vd_mode:
self.return_to_virtual_desktop_1()
def start_whale_browser(self): def start_whale_browser(self):
# Whale 브라우저 실행 # Whale 브라우저 실행
whale_path = r"C:\\Program Files\\Naver\\Naver Whale\\Application\\whale.exe" # whale.exe 경로 지정 whale_path = r"C:\\Program Files\\Naver\\Naver Whale\\Application\\whale.exe" # whale.exe 경로 지정
# 웨일을 시크릿 모드로 실행 # 웨일을 시크릿 모드로 실행
subprocess.Popen([whale_path, '--incognito']) subprocess.Popen([whale_path, '--incognito'])
self.find_whale_window()
pyautogui.hotkey('ctrl', 'l') # 혹은 'ctrl', 'shift'를 사용할 수도 있음 pyautogui.hotkey('ctrl', 'l') # 혹은 'ctrl', 'shift'를 사용할 수도 있음
@ -85,6 +89,15 @@ class WhaleTranslator:
except Exception as e: except Exception as e:
self.logger.debug(f"가상 데스크톱 확인/생성 중 오류 발생: {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): def switch_to_virtual_desktop_2(self):
"""가상 데스크톱 2로 전환""" """가상 데스크톱 2로 전환"""
@ -105,8 +118,8 @@ class WhaleTranslator:
self.logger.debug(f"가상 데스크톱 전환 중 오류 발생: {e}") self.logger.debug(f"가상 데스크톱 전환 중 오류 발생: {e}")
def translate_image(self, url): def translate_image(self, url):
# 가상 데스크톱 2에서 웨일 작업 수행 if self.vd_mode:
self.switch_to_virtual_desktop_2() self.switch_to_virtual_desktop_2()
if self.find_whale_window(): if self.find_whale_window():
win32gui.ShowWindow(self.whale_hwnd, win32con.SW_RESTORE) # 웨일 창 활성화 win32gui.ShowWindow(self.whale_hwnd, win32con.SW_RESTORE) # 웨일 창 활성화
@ -135,7 +148,9 @@ class WhaleTranslator:
pyautogui.typewrite(self.newtab) # URL을 입력 pyautogui.typewrite(self.newtab) # URL을 입력
self.enter_url(self.newtab) self.enter_url(self.newtab)
self.logger.debug(f'번역 완료: {url}') self.logger.debug(f'번역 완료: {url}')
self.return_to_virtual_desktop_1()
if self.vd_mode:
self.return_to_virtual_desktop_1()
else: else:
self.logger.debug('웨일 창을 찾을 수 없습니다.') self.logger.debug('웨일 창을 찾을 수 없습니다.')
@ -188,7 +203,7 @@ class WhaleTranslator:
"""'새 시크릿 탭 - Whale' 창이 존재하면 종료""" """'새 시크릿 탭 - Whale' 창이 존재하면 종료"""
whale_window_name = "새 탭 - Whale" whale_window_name = "새 탭 - Whale"
sec_whale_window_name = "새 시크릿 탭 - Whale" sec_whale_window_name = "새 시크릿 탭 - Whale"
whale_hwnd = self.find_window_by_title(whale_window_name) whale_hwnd = self.find_window_by_title(whale_window_name)
sec_whale_hwnd = self.find_window_by_title(sec_whale_window_name) sec_whale_hwnd = self.find_window_by_title(sec_whale_window_name)

246
옵션요소.txt Normal file
View File

@ -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"
}