559 lines
26 KiB
Python
559 lines
26 KiB
Python
from playwright.sync_api import sync_playwright
|
|
import re
|
|
import pyautogui
|
|
import time
|
|
import win32gui, win32con
|
|
from bs4 import BeautifulSoup
|
|
import pyperclip
|
|
|
|
class BrowserController:
|
|
def __init__(self, app, logger):
|
|
self.app = app
|
|
self.logger = logger
|
|
self.chrome_window_name = "퍼센티 - 셀러들을 위한 AI 구매대행 솔루션 - Chrome"
|
|
# self.whale_window_name = "새 시크릿 탭 - Whale"
|
|
self.chrome_hwnd = None
|
|
self.whale_hwnd = None
|
|
|
|
self.playwright = None
|
|
self.browser = None
|
|
self.page = None
|
|
|
|
def get_page(self):
|
|
return self.page
|
|
|
|
def start_browser(self):
|
|
"""크롬 브라우저 실행 및 페이지 로딩"""
|
|
self.logger.debug('크롬 브라우저 실행 중...')
|
|
|
|
# Playwright를 수동으로 실행하여 브라우저 유지
|
|
self.playwright = sync_playwright().start()
|
|
self.browser = self.playwright.chromium.launch(headless=False) # 브라우저 비헤드리스 모드 실행
|
|
|
|
# 창 크기 설정 (1920x1080)
|
|
context = self.browser.new_context(
|
|
viewport={"width": 1920, "height": 1080}
|
|
)
|
|
|
|
# 페이지 열기
|
|
self.page = context.new_page()
|
|
# self.page.goto('https://percenty.co.kr/') # 원하는 페이지로 이동
|
|
self.page.goto('https://percenty.co.kr/signin') # 원하는 페이지로 이동
|
|
self.logger.debug('newPage 로딩 ...')
|
|
# 사용자는 이 시점에 로그인 및 상세페이지 편집 모드로 들어감
|
|
|
|
# 페이지 제목을 가져와서 창 제목으로 활용
|
|
page_title = self.page.title()
|
|
self.logger.debug(f'페이지 제목: {page_title}')
|
|
|
|
# 창 핸들 찾기 (동적으로 얻은 페이지 제목 사용)
|
|
self.chrome_hwnd = self.find_window_by_title(page_title)
|
|
if not self.chrome_hwnd:
|
|
self.logger.debug('크롬 창을 찾을 수 없습니다.')
|
|
else:
|
|
self.logger.debug(f'크롬 창 핸들: {self.chrome_hwnd}')
|
|
|
|
self.page.wait_for_load_state('networkidle')
|
|
|
|
def login(self, admin_id, user_id, admin_password, user_password, is_admin=False):
|
|
"""로그인 처리"""
|
|
self.logger.debug(f'로그인 시도 중: {"관리자" if is_admin else "직원"} 계정')
|
|
|
|
if is_admin:
|
|
# 관리자 로그인 처리
|
|
self.page.fill('input[placeholder="이메일 주소 입력"]', admin_id) # 관리자 ID 입력
|
|
self.page.fill('input[placeholder="영문/숫자/특수문자의 조합 (6~15자리)"]', admin_password) # 관리자 비밀번호 입력
|
|
self.page.click('button:has-text("로그인 하기")') # 관리자 로그인 버튼 클릭
|
|
else:
|
|
# 관리자 토글 버튼을 클릭해서 직원 로그인 화면 활성화
|
|
admin_toggle = self.page.locator('button[role="switch"]')
|
|
if admin_toggle.get_attribute("aria-checked") == "true":
|
|
admin_toggle.click() # 관리자 모드에서 직원 모드로 전환
|
|
|
|
self.page.fill('input[placeholder="이메일 주소 입력"]', admin_id) # 관리자 ID 입력
|
|
self.page.fill('input[placeholder="직원 아이디 입력"]', user_id) # 직원 ID 입력
|
|
self.page.fill('input[placeholder="영문/숫자/특수문자의 조합 (6~15자리)"]', user_password) # 직원 비밀번호 입력
|
|
self.page.click('button:has-text("직원 로그인 하기")') # 직원 로그인 버튼 클릭
|
|
|
|
self.logger.debug(f'로그인 완료: {"관리자" if is_admin else "직원"} 계정')
|
|
|
|
self.page.wait_for_load_state('networkidle')
|
|
|
|
self.close_ad_if_exists()
|
|
|
|
|
|
def close_browser(self):
|
|
"""브라우저 종료"""
|
|
if self.browser:
|
|
self.browser.close()
|
|
self.playwright.stop()
|
|
self.logger.debug('브라우저 종료됨.')
|
|
|
|
def find_window_by_title(self, window_name):
|
|
"""창 제목을 통해 핸들을 찾는 메서드"""
|
|
def enum_windows_callback(hwnd, result):
|
|
if win32gui.IsWindowVisible(hwnd) and window_name in win32gui.GetWindowText(hwnd):
|
|
result.append(hwnd)
|
|
result = []
|
|
win32gui.EnumWindows(enum_windows_callback, result)
|
|
return result[0] if result else None
|
|
|
|
def switch_to_chrome(self):
|
|
"""크롬으로 포커스 전환"""
|
|
if self.chrome_hwnd:
|
|
win32gui.ShowWindow(self.chrome_hwnd, win32con.SW_RESTORE)
|
|
win32gui.SetForegroundWindow(self.chrome_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:
|
|
self.logger.debug(f"가져온 텍스트: {element_text}") # 텍스트 확인용 로그
|
|
# "총 xx개 상품"에서 숫자만 추출
|
|
count = int(''.join(filter(str.isdigit, element_text)))
|
|
return count
|
|
else:
|
|
self.logger.debug("요소를 찾을 수 없습니다.")
|
|
return 0
|
|
except Exception as e:
|
|
self.logger.debug(f"상품 수를 가져오는 중 오류 발생: {e}", exc_info=True)
|
|
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}", exc_info=True)
|
|
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}", exc_info=True)
|
|
return "수집 오류 발생"
|
|
|
|
def extract_image_urls(self):
|
|
"""HTML에서 이미지 URL 추출 및 img 태그 삭제 후 소스 버튼 다시 클릭"""
|
|
self.logger.debug('이미지 URL을 추출 중...')
|
|
|
|
# 소스 버튼 클릭
|
|
self.page.click("button[data-cke-tooltip-text='소스']")
|
|
self.logger.debug('소스 버튼 클릭 완료.')
|
|
|
|
# 'data-value' 속성 값을 추출 (textarea 요소)
|
|
textarea = self.page.wait_for_selector('div.ck-source-editing-area')
|
|
data_value = textarea.get_attribute("data-value")
|
|
|
|
if data_value:
|
|
self.logger.debug('data-value 속성에서 HTML 수집 완료.')
|
|
|
|
# 이미지 URL 추출
|
|
image_urls = self.fetch_image_urls(data_value)
|
|
self.logger.debug(f'추출된 이미지 URL 수: {len(image_urls)}')
|
|
# 추출된 URL 반환
|
|
self.logger.debug('img 태그를 삭제 중...')
|
|
|
|
self.page.wait_for_load_state('domcontentloaded') # 페이지 로딩 완료 대기
|
|
|
|
# data-value 속성을 가진 요소 선택
|
|
data_value_element = self.page.query_selector('div.ck-source-editing-area')
|
|
|
|
new_value = ""
|
|
|
|
if data_value_element:
|
|
# 속성 변경 (원하는 텍스트로 변경하거나 ""으로 변경)
|
|
# self.page.evaluate('(element, value) => element.setAttribute("data-value", value)', data_value_element, new_value)
|
|
self.page.evaluate(f'() => document.querySelector("div.ck-source-editing-area").setAttribute("data-value", "{new_value}")')
|
|
|
|
# 데이터가 제대로 변경되었는지 확인
|
|
updated_value = data_value_element.get_attribute('data-value')
|
|
self.logger(f'Updated data-value: {updated_value}')
|
|
|
|
else:
|
|
self.logger('Element with data-value not found.')
|
|
|
|
|
|
self.logger.debug('img 태그 삭제 완료.')
|
|
|
|
# 소스 버튼 다시 클릭
|
|
self.page.click("button[data-cke-tooltip-text='소스']")
|
|
self.logger.debug('소스 버튼 재 클릭 완료.')
|
|
|
|
return image_urls
|
|
else:
|
|
self.logger.debug('data-value 속성에 데이터가 없습니다.')
|
|
return []
|
|
|
|
def fetch_image_urls(self, html_content):
|
|
"""
|
|
HTML 콘텐츠에서 모든 <img> 태그의 URL을 순서대로 추출
|
|
"""
|
|
soup = BeautifulSoup(html_content, 'html.parser')
|
|
image_urls = []
|
|
|
|
# class="image_resized"를 가진 모든 <img> 태그 찾기
|
|
images_resized = soup.find_all('img', class_='image_resized')
|
|
for img in images_resized:
|
|
if img and 'src' in img.attrs and img['src'] not in image_urls:
|
|
image_urls.append(img['src'])
|
|
|
|
# <figure class="image"> 내부의 모든 <img> 태그 찾기
|
|
figures = soup.find_all('figure', class_='image')
|
|
for figure in figures:
|
|
img_tag = figure.find('img')
|
|
if img_tag and 'src' in img_tag.attrs and img_tag['src'] not in image_urls:
|
|
image_urls.append(img_tag['src'])
|
|
|
|
return image_urls
|
|
|
|
def close_ad_if_exists(self):
|
|
|
|
"""광고 다이얼로그가 있으면 닫기 버튼을 클릭하는 메서드"""
|
|
try:
|
|
# 광고 다이얼로그가 나타날 때까지 기다림
|
|
dialog_selector = "div.ant-modal-wrap.ant-modal-centered"
|
|
close_button_selector = "div.ant-modal-footer > div > div > button[type='button'].ant-btn.css-1li46mu.ant-btn-default"
|
|
|
|
# 3초 동안 다이얼로그 대기
|
|
self.page.wait_for_selector(dialog_selector, timeout=3000)
|
|
self.logger.debug("다이얼로그가 발견되었습니다. 닫기 버튼을 클릭합니다.")
|
|
|
|
# 닫기 버튼 클릭
|
|
close_button = self.page.query_selector(close_button_selector)
|
|
if close_button:
|
|
close_button.click()
|
|
self.logger.debug("다이얼로그를 성공적으로 닫았습니다.")
|
|
else:
|
|
self.logger.debug("닫기 버튼을 찾지 못했습니다.")
|
|
|
|
except Exception as e:
|
|
# 다이얼로그가 없거나 다른 문제가 발생한 경우
|
|
self.logger.debug(f"다이얼로그가 발견되지 않았거나 오류 발생: {e}", exc_info=True)
|
|
|
|
def go_to_new_product_page(self):
|
|
"""신규 상품 등록 페이지로 이동"""
|
|
try:
|
|
self.page.click('span.ant-menu-title-content:has-text("신규 상품 등록")')
|
|
self.logger.debug("신규 상품 등록 페이지로 이동 완료.")
|
|
except Exception as e:
|
|
self.logger.debug(f"신규 상품 등록 페이지 이동 중 오류: {e}", exc_info=True)
|
|
|
|
def get_product_edit_buttons(self):
|
|
"""현재 페이지의 세부사항 수정 및 업로드 버튼을 찾기"""
|
|
try:
|
|
# 페이지 로딩을 기다림
|
|
self.page.wait_for_load_state('networkidle') # 네트워크 요청이 모두 끝날 때까지 대기
|
|
|
|
# 페이지 끝까지 스크롤하여 모든 동적 요소 로드
|
|
self.scroll_page_to_bottom()
|
|
|
|
|
|
# # 스크롤하여 모든 버튼을 화면에 표시 (가장 하단까지 스크롤)
|
|
# self.page.evaluate("""window.scrollTo(0, document.body.scrollHeight);""")
|
|
# self.logger.debug("페이지를 아래로 스크롤했습니다.")
|
|
|
|
# 버튼 선택 (확실한 선택자를 사용하여 확인)
|
|
buttons = self.page.locator('button:has-text("세부사항 수정 및 업로드")')
|
|
count = buttons.count()
|
|
self.logger.debug(f"수정할 상품 개수: {count}")
|
|
|
|
# 모든 버튼을 리스트로 반환
|
|
return [buttons.nth(i) for i in range(count)]
|
|
|
|
except Exception as e:
|
|
self.logger.debug(f"상품 수정 버튼을 찾는 중 오류: {e}", exc_info=True)
|
|
return []
|
|
|
|
def open_product_edit_dialog(self, button):
|
|
"""상품 수정 다이얼로그 열기"""
|
|
try:
|
|
# 요소가 화면에 없을 경우 스크롤하여 보이도록 함
|
|
button.scroll_into_view_if_needed()
|
|
self.logger.debug("상품의 '세부사항 수정 및 업로드' 버튼을 화면에 보이도록 스크롤.")
|
|
|
|
button.click()
|
|
self.logger.debug("세부사항 수정 다이얼로그 열기 완료.")
|
|
self.page.wait_for_selector('div.ant-tabs-nav') # 다이얼로그가 완전히 로딩될 때까지 기다림
|
|
except Exception as e:
|
|
self.logger.debug(f"세부사항 수정 다이얼로그 열기 중 오류: {e}", exc_info=True)
|
|
|
|
def click_detail_tab(self):
|
|
"""상세페이지 탭 클릭"""
|
|
try:
|
|
self.page.click('div.ant-tabs-tab:has-text("상세페이지")')
|
|
self.logger.debug("상세페이지 탭 클릭 완료.")
|
|
except Exception as e:
|
|
self.logger.debug(f"상세페이지 탭 클릭 중 오류: {e}", exc_info=True)
|
|
|
|
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"옵션 탭 클릭 중 오류: {e}", exc_info=True)
|
|
|
|
def extract_image_urls(self):
|
|
"""상세페이지에서 이미지 URL 추출"""
|
|
try:
|
|
# 소스 편집 모드로 전환
|
|
self.page.click('button[data-cke-tooltip-text="소스"]')
|
|
self.logger.debug("소스 버튼 클릭 완료.")
|
|
|
|
# 'data-value' 속성 값을 추출 (textarea 요소)
|
|
textarea = self.page.wait_for_selector('div.ck-source-editing-area')
|
|
data_value = textarea.get_attribute("data-value")
|
|
|
|
# HTML 소스에서 이미지 URL 추출
|
|
image_urls = self.fetch_image_urls(data_value)
|
|
self.logger.debug(f'추출된 이미지 URL 수: {len(image_urls)}')
|
|
|
|
# HTML 소스에서 이미지 URL 삭제
|
|
self.logger.debug('img 태그를 삭제 중...')
|
|
self.page.wait_for_load_state('domcontentloaded') # 페이지 로딩 완료 대기
|
|
# data-value 속성을 가진 요소 선택
|
|
data_value_element = self.page.query_selector('div.ck-source-editing-area')
|
|
new_value = ""
|
|
if data_value_element:
|
|
# 속성 변경 (원하는 텍스트로 변경하거나 ""으로 변경)
|
|
# self.page.evaluate('(element, value) => element.setAttribute("data-value", value)', data_value_element, new_value)
|
|
self.page.evaluate(f'() => document.querySelector("div.ck-source-editing-area").setAttribute("data-value", "{new_value}")')
|
|
# 데이터가 제대로 변경되었는지 확인
|
|
updated_value = data_value_element.get_attribute('data-value')
|
|
self.logger.debug(f'Updated data-value: {updated_value}')
|
|
else:
|
|
self.logger.debug('Element with data-value not found.')
|
|
self.logger.debug('img 태그 삭제 완료.')
|
|
|
|
|
|
# img 태그의 class 삭제 후 다시 소스 버튼 클릭
|
|
self.page.click('button[data-cke-tooltip-text="소스"]')
|
|
self.logger.debug('소스 버튼 재 클릭 완료.')
|
|
|
|
return image_urls
|
|
except Exception as e:
|
|
self.logger.debug(f"이미지 URL 추출 중 오류: {e}", exc_info=True)
|
|
return []
|
|
|
|
def translate_image(self, url):
|
|
"""이미지 번역 진행"""
|
|
try:
|
|
self.whale_translator.translate_image(url)
|
|
self.logger.debug(f"이미지 번역 완료: {url}")
|
|
except Exception as e:
|
|
self.logger.debug(f"이미지 번역 중 오류: {e}", exc_info=True)
|
|
|
|
def paste_image_in_chrome(self, clipboardImageManager, url):
|
|
"""크롬으로 포커스를 옮기고 클립보드의 이미지를 붙여넣고 엔터 입력"""
|
|
try:
|
|
self.switch_to_chrome() # 크롬으로 포커스 이동
|
|
clipboardImageManager.process_clipboard(url) # 클립보드 내용을 처리
|
|
# clipboard_content = pyperclip.paste()
|
|
if clipboardImageManager.is_clipboard_image():
|
|
pyautogui.hotkey('ctrl', 'v') # 클립보드 이미지 붙여넣기
|
|
pyautogui.press('right') # 오른쪽 입력
|
|
self.logger.debug("이미지 붙여넣기 완료.")
|
|
else:
|
|
self.logger.debug("클립보드가 비어있습니다.")
|
|
except Exception as e:
|
|
self.logger.debug(f"이미지 붙여넣기 중 오류: {e}", exc_info=True)
|
|
|
|
def save_product_edit(self):
|
|
"""상품 수정 후 저장 버튼 클릭"""
|
|
try:
|
|
self.page.click('button:has-text("저장하기")')
|
|
self.logger.debug("상품 수정 내용 저장 완료.")
|
|
self.page.keyboard.press("Escape") # ESC로 다이얼로그 닫기
|
|
except Exception as e:
|
|
self.logger.debug(f"저장 버튼 클릭 중 오류: {e}", exc_info=True)
|
|
|
|
def go_to_next_page(self):
|
|
"""다음 페이지로 이동"""
|
|
try:
|
|
# 현재 페이지가 몇 번째 페이지인지 확인 (클래스에 'ant-pagination-item-active'가 있는 요소)
|
|
current_page = self.page.query_selector('li.ant-pagination-item.ant-pagination-item-active')
|
|
|
|
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"다음 페이지로 이동 중 오류 발생: {e}", exc_info=True)
|
|
return False
|
|
|
|
def switch_to_chrome(self):
|
|
"""크롬으로 포커스 전환"""
|
|
try:
|
|
if not self.chrome_hwnd:
|
|
self.chrome_hwnd = self.find_window_by_title(self.chrome_window_name)
|
|
if self.chrome_hwnd:
|
|
win32gui.ShowWindow(self.chrome_hwnd, win32con.SW_RESTORE)
|
|
win32gui.SetForegroundWindow(self.chrome_hwnd)
|
|
self.logger.debug('크롬 창으로 포커스 이동.')
|
|
self.logger.debug('크롬 창으로 포커스 이동.')
|
|
else:
|
|
self.logger.debug('크롬 창을 찾을 수 없습니다.')
|
|
self.logger.debug('크롬 창을 찾을 수 없습니다.')
|
|
except Exception as e:
|
|
self.logger.debug(f"크롬 포커스 전환 중 오류: {e}", exc_info=True)
|
|
|
|
|
|
|
|
|
|
def scroll_with_wheel(self, direction="down", pause_time=0.5, max_scrolls=20):
|
|
"""
|
|
휠 스크롤을 사용하여 페이지를 위나 아래로 천천히 스크롤.
|
|
|
|
Parameters:
|
|
- direction: 스크롤 방향 ("down"은 아래로, "up"은 위로).
|
|
- pause_time: 스크롤 사이의 대기 시간 (초).
|
|
- max_scrolls: 최대 스크롤 횟수.
|
|
"""
|
|
scroll_count = 0
|
|
last_height = self.page.evaluate("document.body.scrollHeight")
|
|
|
|
while scroll_count < max_scrolls:
|
|
if direction == "down":
|
|
# 아래로 스크롤
|
|
self.page.mouse.wheel(0, 1000)
|
|
elif direction == "up":
|
|
# 위로 스크롤
|
|
self.page.mouse.wheel(0, -1000)
|
|
else:
|
|
raise ValueError("direction 인자는 'down' 또는 'up'만 허용됩니다.")
|
|
|
|
time.sleep(pause_time)
|
|
|
|
# 스크롤 후 높이 확인
|
|
new_height = self.page.evaluate("document.body.scrollHeight")
|
|
|
|
# 아래로 스크롤 시, 페이지의 끝에 도달한 경우 종료
|
|
if direction == "down" and new_height == last_height:
|
|
break
|
|
elif direction == "up" and new_height == 0:
|
|
break # 위로 스크롤 시, 페이지의 시작에 도달하면 종료
|
|
|
|
last_height = new_height
|
|
scroll_count += 1
|
|
|
|
def collect_product_info(self):
|
|
"""
|
|
상품 정보를 수집하는 메서드
|
|
"""
|
|
try:
|
|
# 페이지를 아래로 스크롤하여 모든 상품 로드
|
|
self.scroll_with_wheel('down')
|
|
self.scroll_with_wheel('up')
|
|
|
|
product_infos = []
|
|
for i in range(1, 51): # 1부터 최대 50까지 상품 처리
|
|
try:
|
|
# 각 상품의 CSS 선택자를 동적으로 생성하여 접근
|
|
product_name_selector = f"div#root div:nth-child({i}) > div > li > div > div > div:nth-child(2) > div > div > div.ant-col.css-1li46mu > div.sc-dPZUQH.mXDuy > span.sc-dSIIpw.Nrwqu.Body3Regular14.CharacterPrimary85"
|
|
product_price_selector = f"div#root div:nth-child({i}) > div > li > div > div > div:nth-child(2) > div > div > div.ant-col.css-1li46mu > div:nth-child(3) > div:nth-child(1) > span.sc-dSIIpw.Nrwqu.Body3Regular14.CharacterPrimary85"
|
|
product_image_selector = f"div#root div:nth-child({i}) > div > li > div > div > div:nth-child(1) > div > div:nth-child(2) > div > div > img"
|
|
|
|
product_name_element = self.page.locator(product_name_selector)
|
|
product_price_element = self.page.locator(product_price_selector)
|
|
product_image_element = self.page.locator(product_image_selector)
|
|
|
|
if product_name_element and product_price_element and product_image_element:
|
|
product_info = {
|
|
"name": product_name_element.text_content().strip(),
|
|
"price": product_price_element.text_content().strip(),
|
|
"image_url": product_image_element.get_attribute('src')
|
|
}
|
|
self.logger.debug(f"상품 {i}: {product_info}")
|
|
product_infos.append(product_info)
|
|
except Exception as e:
|
|
self.logger.error(f"상품 {i} 정보 수집 중 오류 발생: {e}", exc_info=True)
|
|
continue
|
|
|
|
return product_infos
|
|
except Exception as e:
|
|
self.logger.error(f"상품 정보 수집 중 오류 발생: {e}", exc_info=True)
|
|
return []
|
|
|
|
def click_modify_button_by_text(self, index):
|
|
"""인덱스에 해당하는 '세부사항 수정 및 업로드' 버튼 클릭"""
|
|
try:
|
|
button_selector = f'(//button[span[text()="세부사항 수정 및 업로드"]])[{index}]'
|
|
|
|
# 버튼이 화면에 보이도록 스크롤 후 클릭
|
|
button = self.page.query_selector(button_selector)
|
|
if button:
|
|
button.scroll_into_view_if_needed()
|
|
self.page.evaluate('arguments[0].click();', button)
|
|
self.logger.debug(f'{index}번째 상품의 수정 버튼 클릭 완료')
|
|
else:
|
|
self.logger.debug(f'{index}번째 상품의 수정 버튼을 찾지 못했습니다.')
|
|
except Exception as e:
|
|
self.logger.debug(f'{index}번째 상품의 수정 버튼 클릭 중 오류: {str(e)}')
|
|
|
|
|
|
def scroll_page_to_bottom(self, pause_time=1):
|
|
"""페이지의 맨 아래까지 스크롤하여 모든 동적 요소를 로드"""
|
|
self.logger.debug('페이지 스크롤 시작...')
|
|
previous_height = self.page.evaluate("() => document.body.scrollHeight")
|
|
|
|
while True:
|
|
self.page.evaluate("window.scrollBy(0, window.innerHeight);") # 한 화면씩 스크롤
|
|
time.sleep(pause_time) # 페이지 로딩 대기
|
|
current_height = self.page.evaluate("() => document.body.scrollHeight")
|
|
if current_height == previous_height:
|
|
break # 더 이상 스크롤할 내용이 없으면 종료
|
|
previous_height = current_height
|
|
self.logger.debug('페이지 스크롤 완료.')
|
|
|
|
|