ChangePercenty2/utils/playwright_helpers.py

503 lines
27 KiB
Python

from playwright.async_api import async_playwright
import os, random, traceback
class PlaywrightHelper:
def __init__(self):
self.browser = None
self.context = None
self.page = None
self.market_names = [
'쿠팡', '스마트스토어', 'esm', '11번가-국내', '11번가-글로벌', '롯데온', '인터파크', '위메프', '옥션1.0'
]
self.market_xpaths = {
'쿠팡': "div#rc-tabs-0-tab-cp",
'스마트스토어': "div#rc-tabs-0-tab-ss",
'esm': "div#rc-tabs-0-tab-esm",
'11번가-국내': "div#rc-tabs-0-tab-est",
'11번가-글로벌': "div#rc-tabs-0-tab-est_global",
'롯데온': "div#rc-tabs-0-tab-lotteon",
'인터파크': "div#rc-tabs-0-tab-ip",
'위메프': "div#rc-tabs-0-tab-wmp",
'옥션1.0': "div#rc-tabs-0-tab-at"
}
self.progress_steps = {
'쿠팡': 35,
'스마트스토어': 40,
'esm': 45,
'11번가-국내': 50,
'11번가-글로벌': 55,
'롯데온': 60,
'인터파크': 65,
'위메프': 70,
'옥션1.0': 75
}
# 마켓 이름과 XPath 매핑
self.market_xpath_mapping = {
'쿠팡': 'cp',
'스마트스토어': 'ss',
'esm': 'esm',
'11번가-국내': 'est',
'11번가-글로벌': 'est_global',
'롯데온': 'lotteon',
'인터파크': 'ip',
'위메프': 'wmp',
'옥션1.0': 'at'
}
self.api_key_validation_button = {
'쿠팡': "div#rc-tabs-0-panel-cp div.ant-row.css-1li46mu > button[type=\"button\"].ant-btn.css-1li46mu.ant-btn-primary",
'스마트스토어': "div#rc-tabs-0-panel-ss div.ant-row.css-1li46mu > button[type=\"button\"]:nth-child(3)",
'esm': "div#rc-tabs-0-panel-esm div.ant-row.css-1li46mu > button[type=\"button\"].ant-btn.css-1li46mu.ant-btn-primary",
'11번가-국내': "div#rc-tabs-0-panel-est div.ant-row.css-1li46mu > button[type=\"button\"].ant-btn.css-1li46mu.ant-btn-primary",
'11번가-글로벌': "div#rc-tabs-0-panel-est_global div.ant-row.css-1li46mu > button[type=\"button\"].ant-btn.css-1li46mu.ant-btn-primary",
'롯데온': "div#rc-tabs-0-panel-lotteon div.ant-row.css-1li46mu > button[type=\"button\"].ant-btn.css-1li46mu.ant-btn-primary",
'인터파크': "div#rc-tabs-0-panel-ip div.ant-row.css-1li46mu > button[type=\"button\"].ant-btn.css-1li46mu.ant-btn-primary",
'위메프': "div#rc-tabs-0-panel-wmp div.ant-row.css-1li46mu > button[type=\"button\"].ant-btn.css-1li46mu.ant-btn-primary",
'옥션1.0': "div#rc-tabs-0-panel-at div.ant-row.css-1li46mu > button[type=\"button\"].ant-btn.css-1li46mu.ant-btn-primary",
}
self.api_key_status = {
'쿠팡': "div#rc-tabs-0-panel-cp span:nth-child(2) > sup",
'스마트스토어': "div#rc-tabs-0-panel-ss span:nth-child(2) > sup",
'esm': "div#rc-tabs-0-panel-esm span:nth-child(2) > sup",
'11번가-국내': "div#rc-tabs-0-panel-est span:nth-child(2) > sup",
'11번가-글로벌': "div#rc-tabs-0-panel-est_global span:nth-child(2) > sup",
'롯데온': "div#rc-tabs-0-panel-lotteon span:nth-child(2) > sup",
'인터파크': "div#rc-tabs-0-panel-ip span:nth-child(2) > sup",
'위메프': "div#rc-tabs-0-panel-wmp span:nth-child(2) > sup",
'옥션1.0': "div#rc-tabs-0-panel-at sup",
}
self.order_collection_button = {
'쿠팡': "div#rc-tabs-0-panel-cp div:nth-child(5) > button[type=\"button\"]",
# '스마트스토어': "div#rc-tabs-0-panel-ss div:nth-child(4) > button[type=\"button\"]",
'스마트스토어': "#rc-tabs-0-panel-ss .css-1li46mu[role='switch'][type='button']",
'esm': "div#rc-tabs-0-panel-esm div:nth-child(3) > button[type=\"button\"]",
'11번가-국내': "div#rc-tabs-0-panel-est div > div > div:nth-child(2) > button[type=\"button\"]",
'11번가-글로벌': "div#rc-tabs-0-panel-est_global div > div > div:nth-child(2) > button[type=\"button\"]",
'롯데온': "div#rc-tabs-0-panel-lotteon div > div > div:nth-child(2) > button[type=\"button\"]",
'인터파크': "div#rc-tabs-0-panel-ip div:nth-child(25) > button[type=\"button\"]",
'위메프': "div#rc-tabs-0-panel-wmp div:nth-child(2) > button[type=\"button\"]",
'옥션1.0': "div#rc-tabs-0-panel-at div:nth-child(1) > button[type=\"button\"]",
}
self.smartstore_elements = {
# 'account_button': "div#rc-tabs-0-panel-ss div.ant-row.css-1li46mu > button[type=\"button\"].ant-btn.css-1li46mu.ant-btn-primary.ant-btn-background-ghost",
'account_button': "#rc-tabs-0-panel-ss .css-1li46mu .ant-btn-background-ghost[type='button']",
# 'popup_login_type_button': "div#root div.Login_login_content__Ia6Rm > ul > li:nth-child(1)",
'popup_login_type_button': ".Login_type_list__KPqjy .Login_type_item__2_QV8 .Login_title__3ixGa",
# 'popup_login_id_input': "div#root li.Login_login_item__2fOX0.Login_hover__2Wdak.Login_on__A5e8I > input",
'popup_login_id_input': ".Login_login_area__cMnCU input[placeholder='아이디 또는 이메일 주소']",
# 'popup_login_pw_input': "div#root li:nth-child(2) > input",
'popup_login_pw_input': ".Login_login_item__2fOX0 [placeholder='비밀번호']",
# 'popup_login_button': "div#root div.Login_login_content__Ia6Rm > div > div > button[type=\"button\"]",
'popup_login_button': ".Login_btn_box__22wei [type='button']",
# 'login_success_check': "div#_gnb_nav span.login-id.text-overflow"
'login_success_check': "[ng-if='vm.loginInfo'] .login-id.text-overflow"
}
self.toggle_states = {
'쿠팡': 'on',
'스마트스토어': 'on',
'esm': 'on',
'11번가-국내': 'on',
'11번가-글로벌': 'on',
'롯데온': 'on',
'인터파크': 'on',
'위메프': 'off',
'옥션1.0': 'off',
}
self.api_key_xpaths = {
'쿠팡': {
'쿠팡id': "div#rc-tabs-0-panel-cp div:nth-child(1) > input",
'업체코드': "div#rc-tabs-0-panel-cp div:nth-child(2) > input",
'accesskey': "div#rc-tabs-0-panel-cp div:nth-child(3) > input",
'secretkey': "div#rc-tabs-0-panel-cp div:nth-child(4) > input",
},
'스마트스토어': {
'업로드할스마트스토어계정명' : "div#rc-tabs-0-panel-ss div:nth-child(1) > input",
'업로드할스마트스토어계정id' : "div#rc-tabs-0-panel-ss div:nth-child(1) > input",
'업로드할스마트스토어계정pw' : "div#rc-tabs-0-panel-ss div:nth-child(1) > input",
'애플리케이션id': "div#rc-tabs-0-panel-ss div:nth-child(2) > input",
'애플리케이션시크릿': "div#rc-tabs-0-panel-ss div:nth-child(3) > input",
},
'esm': {
'옥션id': "div#rc-tabs-0-panel-esm div:nth-child(1) > input",
'지마켓id': "div#rc-tabs-0-panel-esm div:nth-child(2) > input",
},
'11번가-국내': {
'apikey': "div#rc-tabs-0-panel-est input",
},
'11번가-글로벌': {
'apikey': "div#rc-tabs-0-panel-est_global input",
},
'롯데온': {
'apikey': "div#rc-tabs-0-panel-lotteon input",
},
'인터파크': {
'상품상태재고수정인증키': "div#rc-tabs-0-panel-ip div:nth-child(1) > input",
'상품상태재고수정비밀키':"div#rc-tabs-0-panel-ip div:nth-child(2) > input",
'상품재고조회인증키':"div#rc-tabs-0-panel-ip div:nth-child(3) > input",
'상품재고조회비밀키':"div#rc-tabs-0-panel-ip div:nth-child(4) > input",
'상품정보조회인증':"div#rc-tabs-0-panel-ip div:nth-child(6) > input",
'상품정보조회비밀키':"div#rc-tabs-0-panel-ip div:nth-child(7) > input",
'상품수정인증키':"div#rc-tabs-0-panel-ip div:nth-child(8) > input",
'상품수정비밀키':"div#rc-tabs-0-panel-ip div:nth-child(9) > input",
'상품등록인증키':"div#rc-tabs-0-panel-ip div:nth-child(11) > input",
'상품등록비밀키':"div#rc-tabs-0-panel-ip div:nth-child(12) > input",
'반품배송지조회인증키':"div#rc-tabs-0-panel-ip div:nth-child(13) > input",
'반품배송지조회비밀키':"div#rc-tabs-0-panel-ip div:nth-child(14) > input",
'반품배송지등록인증키':"div#rc-tabs-0-panel-ip div:nth-child(16) > input",
'반품배송지등록비밀키':"div#rc-tabs-0-panel-ip div:nth-child(17) > input",
'상품qna등록인증키':"div#rc-tabs-0-panel-ip div:nth-child(18) > input",
'상품qna등록비밀키':"div#rc-tabs-0-panel-ip div:nth-child(19) > input",
'상품qna조회인증키':"div#rc-tabs-0-panel-ip div:nth-child(21) > input",
'상품qna조회비밀키':"div#rc-tabs-0-panel-ip div:nth-child(22) > input",
'인터파크업체번호':"div#rc-tabs-0-panel-ip div:nth-child(23) > input",
'공급계약일련번호':"div#rc-tabs-0-panel-ip div:nth-child(24) > input",
},
'위메프': {
'apikey': "div#rc-tabs-0-panel-wmp input",
},
'옥션1.0': {
'apikey': "div#rc-tabs-0-panel-at div:nth-child(1) > input",
'멤버id': "div#rc-tabs-0-panel-at div:nth-child(2) > input",
},
}
async def init_browser(self, isHeadless_mode = True):
try:
"""브라우저 설정 및 인스턴스 생성"""
playwright = await async_playwright().start()
# self.browser = await playwright.chromium.launch(headless=False)
# 드라이버의 경로 설정
driver_path = os.path.join(os.path.dirname(__file__), 'drivers', 'chromium-1112', 'chrome-win','chrome.exe')
# 확장 프로그램 경로
extension_path = os.path.join(os.path.dirname(__file__), 'drivers', 'extensions', '1.1.100_0')
# 사용자 데이터 디렉토리 설정
user_data_dir = os.path.join(os.path.dirname(__file__), 'drivers', 'user_data')
# User agent 설정
user_agent = random.choice([
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/109.0.0.0 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/109.0.0.0 Safari/537.36 Edg/109.0.0.0",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:108.0) Gecko/20100101 Firefox/108.0",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 12_0) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.0 Safari/605.1.15",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/109.0.0.0 Safari/537.36 OPR/85.0.0.0",
])
self.browser = await playwright.chromium.launch_persistent_context(user_data_dir,
headless=isHeadless_mode,
permissions=["geolocation", "notifications"], # 필요한 권한을 모두 허용
geolocation={"latitude": 37.5665, "longitude": 126.9780}, # 위치 정보 설정 (서울)
locale="ko-KR", # 로케일 설정
args=[
'--disable-popup-blocking', # 팝업 차단 비활성화
f'--disable-extensions-except={extension_path}',
f'--load-extension={extension_path}'
],
executable_path=driver_path,
user_agent=user_agent # user agent 설정 추가
) # headless 모드 설정
# self.page = await self.browser.new_page() # 첫 번째 페이지를 가져옴
self.page = self.browser.pages[0]
# await self.page.set_default_navigation_timeout(40000) # 기본 타임아웃(40초) 변경
except Exception as e:
print(f"브라우저 초기화 중 에러발생 : {e}")
traceback.print_exc()
async def close_browser(self):
if self.browser:
await self.browser.close()
async def login_and_fetch_api_keys(self, url, username, password, status_label, progress_bar):
try:
print('로그인 동작 실행')
await self.login_Process(url, username, password, progress_bar, status_label)
print('로그인 동작 완료')
# # context = await self.browser.new_context()
# # page = await context.new_page()
# # self.page = await self.browser.new_page() # 첫 번째 페이지를 가져옴
# await self.page.goto(url)
# progress_bar.setValue(10)
# status_label.setText('현재 상태: 웹페이지 접속완료')
# # 현재 로그인 상태 확인
# # login_status = await self.page.query_selector("div#root div:nth-child(3) > span")
# login_status = await self.page.query_selector(".ant-space-item .ant-typography:nth-child(2)", 2000)
# if not login_status:
# print('로그인 실행')
# # 홈 페이지에서 로그인 버튼 클릭
# await self.page.click(".signList > .ant-btn-default > span")
# # 로그인 페이지에서 로그인 수행
# await self.page.fill(".ant-input:nth-child(4)", username)
# await self.page.fill(".ant-input:nth-child(1)", password)
# await self.page.click(".ant-btn-primary")
# progress_bar.setValue(20)
# status_label.setText('현재 상태: 퍼센티 로그인 완료')
# # 마켓 설정 페이지로 이동
# await self.page.click("xpath=/html/body/div[1]/div/div/div/div/aside/div/ul/li[7]/ul/li[2]")
# # await self.page.click("div#root li.ant-menu-item.ant-menu-item-selected.ant-menu-item-only-child > span")
# # 마켓 설정상 팝업 다이얼로그가 있는 경우 닫기 (있는 경우)
# try:
# # await self.page.click(".ant-modal-footer [type='button']")
# await self.page.click("div.ant-modal-footer > button[type=\"button\"].ant-btn.css-1li46mu.ant-btn-primary",2)
# # .ant-modal-content
# # .ant-modal-footer [type='button']
# except:
# pass
# progress_bar.setValue(30)
# status_label.setText('현재 상태: 마켓 설정')
api_keys = {}
for market in self.market_names:
print(f"self.market_xpaths[market] : {self.market_xpaths[market]}")
print(f"self.api_key_xpaths[market] : {self.api_key_xpaths[market]}")
api_keys[market] = await self.fetch_market_api_keys(self.market_xpaths[market], self.api_key_xpaths[market])
print(f"api_keys['{market}'] \n {api_keys[market]}")
progress_bar.setValue(self.progress_steps[market])
status_label.setText(f'현재 상태: {market} 가져오기 완료')
return api_keys
except Exception as e:
print(f"오류발생 : {e}")
# 각 마켓별 API 키 가져오기
async def fetch_market_api_keys(self, market_xpath, api_key_xpaths):
await self.page.click(market_xpath)
await self.page.wait_for_timeout(1000) # 잠시 대기
return {key: await self.page.get_attribute(xpath, 'value') for key, xpath in api_key_xpaths.items()}
async def login_check(self):
try:
print("login_check 실행")
await self.page.wait_for_load_state() # 페이지의 모든 요소가 로드될때 까지 대기
print("웹 로딩 완료")
await self.page.wait_for_selector("div#root div:nth-child(3) > span", timeout=2000) # 2초 내 로그인 여부 판단
return True
except:
return False
async def login_Process(self, url, username, password, progress_bar, status_label):
try:
await self.page.goto(url)
progress_bar.setValue(10)
status_label.setText('현재 상태: 웹페이지 접속완료')
# 현재 로그인 상태 확인
# login_status = await self.page.query_selector("div#root div:nth-child(3) > span")
# login_status = await self.page.query_selector(".ant-space-item .ant-typography:nth-child(2)", timeout=3000)
# login_status = await self.page.query_selector("#root > div > div > div > header > div > div.ant-space.css-1li46mu.ant-space-horizontal.ant-space-align-center.ant-space-gap-row-small.ant-space-gap-col-small > div:nth-child(3) > span", timeout=3000)
login_status = await self.login_check()
print(f"login_status : {login_status}")
if not login_status:
print('로그인 실행')
# 홈 페이지에서 로그인 버튼 클릭
await self.page.click(".signList > .ant-btn-default > span")
# 로그인 페이지에서 로그인 수행
await self.page.fill(".ant-input:nth-child(4)", username)
await self.page.fill(".ant-input:nth-child(1)", password)
await self.page.click(".ant-btn-primary")
progress_bar.setValue(20)
status_label.setText('현재 상태: 퍼센티 로그인 완료')
# 마켓 설정 페이지로 이동
await self.page.click("xpath=/html/body/div[1]/div/div/div/div/aside/div/ul/li[7]/ul/li[2]")
print("마켓 설정 페이지로 이동")
# await self.page.click("[role='menu'] .ant-menu-item.ant-menu-item-selected")
# 팝업 다이얼로그 닫기 (있는 경우)
try:
# await self.page.click("xpath=body > div:nth-child(10) > div > div.ant-modal-wrap.ant-modal-centered > div > div.ant-modal-content > div.ant-modal-footer > button.ant-btn.css-1li46mu.ant-btn-primary")
print("팝업다이알로그가 존재한다면 클릭")
await self.page.click("div.ant-modal-footer > button[type=\"button\"].ant-btn.css-1li46mu.ant-btn-primary",timeout=1000)
except:
pass
progress_bar.setValue(30)
status_label.setText('현재 상태: 마켓 설정')
except Exception as e:
print(f"오류발생 : {e}")
async def update_api_keys(self, url, username, password, api_keys, status_label, progress_bar):
try:
print('로그인 동작 실행')
await self.login_Process(url, username, password, progress_bar, status_label)
print('로그인 동작 완료')
# await self.page.goto(url)
# progress_bar.setValue(10)
# status_label.setText('현재 상태: 웹페이지 접속완료')
# # 현재 로그인 상태 확인
# # login_status = await self.page.query_selector("div#root div:nth-child(3) > span")
# login_status = await self.page.query_selector(".ant-space-item .ant-typography:nth-child(2)", 2000)
# if not login_status:
# print('로그인 실행')
# # 홈 페이지에서 로그인 버튼 클릭
# await self.page.click(".signList > .ant-btn-default > span")
# # 로그인 페이지에서 로그인 수행
# await self.page.fill(".ant-input:nth-child(4)", username)
# await self.page.fill(".ant-input:nth-child(1)", password)
# await self.page.click(".ant-btn-primary")
# progress_bar.setValue(20)
# status_label.setText('현재 상태: 퍼센티 로그인 완료')
# # 마켓 설정 페이지로 이동
# await self.page.click("xpath=/html/body/div[1]/div/div/div/div/aside/div/ul/li[7]/ul/li[2]")
# # 팝업 다이얼로그 닫기 (있는 경우)
# try:
# await self.page.click("xpath=body > div:nth-child(10) > div > div.ant-modal-wrap.ant-modal-centered > div > div.ant-modal-content > div.ant-modal-footer > button.ant-btn.css-1li46mu.ant-btn-primary")
# await self.page.click("div.ant-modal-footer > button[type=\"button\"].ant-btn.css-1li46mu.ant-btn-primary",2)
# except:
# pass
# 각 마켓의 API 키를 업데이트
for market in self.market_xpath_mapping.keys():
if market in api_keys:
await self.update_market_api_keys(market, api_keys[market])
progress_bar.setValue(progress_bar.value() + 5)
status_label.setText(f'현재 상태: {market} 업데이트 완료')
await self.browser.close()
except Exception as e:
print(f"오류발생 : {e}")
# 각 마켓별 API 키 업데이트
async def update_market_api_keys(self, market, market_api_keys):
print(f"update_market_api_keys || market : {market} 시도")
market_xpath = f"div#rc-tabs-0-tab-{self.market_xpath_mapping[market]}"
api_key_xpaths = self.api_key_xpaths[market]
print(f"update_market_api_keys || market_xpath : {market_xpath} ")
print(f"update_market_api_keys || api_key_xpaths : {api_key_xpaths} ")
await self.page.click(market_xpath)
await self.page.wait_for_timeout(1000) # 잠시 대기
# 주문 수집 기능 설정
print("toggle_state 시작")
toggle_state = await self.page.get_attribute(self.order_collection_button[market], "aria-checked")
print(f"toggle_state : {toggle_state}, self.toggle_states[market] : {self.toggle_states[market]}, ")
if self.toggle_states[market] == 'on' and toggle_state == 'false':
await self.page.click(self.order_collection_button[market])
elif self.toggle_states[market] == 'off' and toggle_state == 'true':
await self.page.click(self.order_collection_button[market])
# API 키 입력
for key, value in api_key_xpaths.items():
api_key_value = market_api_keys.get(key, '')
print(f"key : {key}")
print(f"value : {value}")
print(f"api_key_value : {api_key_value}")
if key not in ['업로드할스마트스토어계정명', '업로드할스마트스토어계정id', '업로드할스마트스토어계정pw']:
print(f"key : {key}이므로 fill 실행")
await self.page.fill(value, api_key_value)
# 스마트스토어의 경우 로그인 작업 수행
if market == '스마트스토어' and key == '업로드할스마트스토어계정명':
print(f"스마트스토어 작업 시작")
# 새 창이 열리는 이벤트를 기다림
async with self.page.context.expect_page() as new_page_info: # 새 팝업이 아닌 새창
await self.page.click(self.smartstore_elements['account_button'])
print(f"page.click - account_button")
popup_page = await new_page_info.value
print(f"새 창 열림: {popup_page.url}")
await popup_page.wait_for_load_state()
print("새 창 로드 완료")
await popup_page.evaluate('document.querySelector(arguments[0]).scrollIntoViewIfNeeded()', self.smartstore_elements['popup_login_type_button'])
await popup_page.click(self.smartstore_elements['popup_login_type_button'])
print(f"page.click - popup_login_type_button")
await popup_page.evaluate('document.querySelector(arguments[0]).scrollIntoViewIfNeeded()', self.smartstore_elements['popup_login_id_input'])
await popup_page.fill(self.smartstore_elements['popup_login_id_input'], market_api_keys.get('업로드할스마트스토어계정id', ''))
print(f"page.fill - 업로드할스마트스토어계정id")
await popup_page.fill(self.smartstore_elements['popup_login_pw_input'], market_api_keys.get('업로드할스마트스토어계정pw', ''))
print(f"page.fill - 업로드할스마트스토어계정pw")
await popup_page.evaluate('document.querySelector(arguments[0]).scrollIntoViewIfNeeded()', self.smartstore_elements['popup_login_button'])
await popup_page.click(self.smartstore_elements['popup_login_button'])
print(f"page.click - popup_login_button")
try:
# 팝업 페이지가 닫혀 있는지 확인
if popup_page.is_closed():
print("팝업 페이지가 닫혔습니다. 로그인 성공으로 간주합니다.")
else:
# 로그인 성공 확인
login_success = await popup_page.inner_text(self.smartstore_elements['login_success_check'])
if login_success != market_api_keys.get('업로드할스마트스토어계정id', ''):
print("스마트스토어 로그인 실패")
return
# 팝업 닫기
await popup_page.close()
except Exception as e:
print(f"로그인 팝업윈도우 처리 중 에러발생 : {e}")
traceback.print_exc()
# API 검증 버튼 클릭
await self.page.click(self.api_key_validation_button[market])
await self.page.wait_for_timeout(2000) # 잠시 대기
# API 상태 확인
api_status = await self.page.inner_text(self.api_key_status[market])
print(f"{market} API 상태 : {api_status}")
if "검증 완료" not in api_status:
print(f"{market} API 검증 실패")
return