ChangePercenty2/utils/playwright_helpers.py

663 lines
36 KiB
Python

from playwright.async_api import async_playwright
import os, random, traceback, asyncio
from ui.two_factor_auth_dialog import TwoFactorAuthDialog
from playwright._impl._errors import TargetClosedError
class PlaywrightHelper:
def __init__(self):
self.browser = None
self.context = None
self.page = None
self.dialog = TwoFactorAuthDialog()
self.username = None
self.password = None
self.progress_bar = None
self.status_label = 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\"]",
'스마트스토어': "#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': "#rc-tabs-0-panel-ss .css-1li46mu .ant-btn-background-ghost[type='button']",
'popup_login_type_button': ".Login_type_list__KPqjy .Login_type_item__2_QV8 .Login_title__3ixGa",
'popup_login_id_input': ".Login_login_area__cMnCU input[placeholder='아이디 또는 이메일 주소']",
'popup_login_pw_input': ".Login_login_item__2fOX0 [placeholder='비밀번호']",
'popup_login_button': ".Login_btn_box__22wei [type='button']",
'login_success_check': "[ng-if='vm.loginInfo'] .login-id.text-overflow",
'2fa_check': '#twoFactorAuth', # 2단계 인증 확인용 셀렉터
'2fa_complete_check': '#twoFactorAuthComplete' # 2단계 인증 완료 확인용 셀렉터
}
self.toggle_states = {
'쿠팡': 'on',
'스마트스토어': 'on',
'esm': 'on',
'11번가-국내': 'on',
'11번가-글로벌': 'on',
'롯데온': 'on',
'인터파크': 'on',
'위메프': 'off',
'옥션1.0': 'off',
}
self.market_shipping_profile_selectors = {
'쿠팡': ('cp', 'sc-enhgma.iWXfFP'),
'스마트스토어': ('ss', 'sc-ialZcF.AOoIo'),
'esm': ('esm', 'sc-DIosr.gWSxuU'),
'11번가-국내': ('est', 'sc-jwiwYR.juQnsn'),
'11번가-글로벌': ('est_global', 'sc-eLoUSf.dcgEMZ'),
'롯데온': ('lotteon', 'sc-ldRCOE.eQdovA'),
'인터파크': ('ip', 'sc-kYKKLo.bKCTmC'),
'위메프': ('wmp', 'sc-jzrJLD.jMimQL'),
'옥션1.0': ('at', 'sc-hLiTId.fFaLDf')
}
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')
# 사용자 데이터 디렉토리가 존재하지 않으면 생성
if not os.path.exists(user_data_dir):
os.makedirs(user_data_dir)
print(f"{user_data_dir} 디렉토리가 생성되었습니다.")
# 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 launch_new_browser(self, isHeadless_mode = False):
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')
# 사용자 데이터 디렉토리가 존재하지 않으면 생성
if not os.path.exists(user_data_dir):
os.makedirs(user_data_dir)
print(f"{user_data_dir} 디렉토리가 생성되었습니다.")
# 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.context = 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.context.new_page()
await self.page.goto('https://www.percenty.co.kr')
# 브라우저 창 크기 조정
await self.page.set_viewport_size({"width": 1920, "height": 1080})
# 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()
print("self.browser 종료 실행 완료")
async def clear_cache(self):
# 브라우저 캐시를 삭제하는 메서드
if self.context:
await self.context.clear_cookies()
await self.context.clear_storage()
async def set_headless_mode(self, headless):
print(f"헤드리스 모드 : {headless}")
await self.browser.close()
print(f"기존 브라우저 닫기")
await self.launch_new_browser(headless)
# print(f"새 브라우저 headless모드 : {headless}로 초기화")
# await self.login_Process('https://percenty.co.kr', self.username, self.password, self.progress_bar, self.status_label)
print(f"브라우저 로그인")
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('로그인 동작 완료')
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) # 잠시 대기
await asyncio.sleep(1) # 잠시 대기
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 .ant-typography.css-1li46mu:nth-child(3) > span", timeout=2000) # 2초 내 로그인 여부 판단
await self.page.wait_for_selector("div#root .ant-space-item .ant-typography.css-1li46mu[style='color: rgba(0, 0, 0, 0.85); font-size: 14px;']", timeout=2000) # 2초 내 로그인 여부 판단
return True
except:
return False
async def login_Process(self, url, username, password, progress_bar, status_label):
try:
self.username = username
self.password = password
await self.page.goto(url)
progress_bar.setValue(10)
status_label.setText('현재 상태: 웹페이지 접속완료')
# 현재 로그인 상태 확인
login_status = await self.login_check()
print(f"login_status : {login_status}")
if not login_status:
print('로그인 실행')
# 홈 페이지에서 로그인 버튼 클릭
# 로그인 버튼이 보이도록 가로 스크롤
await self.page.evaluate('window.scrollBy(800, 0)') # 가로 스크롤
await self.page.wait_for_selector(".signList > .ant-btn-default > span", state='visible')
print('로그인 페이지 스크롤')
await self.page.click(".signList > .ant-btn-default > span")
# div#root .css-1li46mu .hojyII .signList button.ant-btn.ant-btn-default[type='button'] span
print('로그인 페이지의 로그인 버튼 클릭')
# 로그인 페이지에서 로그인 수행
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")
# 알림 메시지가 나타나면 닫기
try:
await self.page.wait_for_selector("#ch-shadow-root-wrapper > div.ModalLayer__ModalLayerWrapper-ch-front__sc-10u1w92-0.jImPdL > div > article > div > div > div.PopupCloseBtn__CloseButtonArea-ch-front__sc-14jjsiy-2.kSHXDH > button", timeout=2000)
await self.page.click("#ch-shadow-root-wrapper > div.ModalLayer__ModalLayerWrapper-ch-front__sc-10u1w92-0.jImPdL > div > article > div > div > div.PopupCloseBtn__CloseButtonArea-ch-front__sc-14jjsiy-2.kSHXDH > button")
print("알림 메시지를 닫았습니다.")
except Exception as e:
print("알림 메시지가 나타나지 않았습니다.")
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("마켓 설정 페이지로 이동")
# 팝업 다이얼로그 닫기 (있는 경우)
try:
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:
# 캐시 삭제
await self.clear_cache()
print('캐쉬 삭제')
await self.login_Process(url, username, password, progress_bar, status_label)
print('로그인 동작 완료')
# 각 마켓의 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()
print('browser 종료')
return True
except Exception as e:
print(f"오류발생 : {e}")
return False
# 각 마켓별 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) # 잠시 대기
await asyncio.sleep(1) # 잠시 대기
# 배송프로필 선택
await self.select_shipping_profile(market, market_api_keys)
# 주문 수집 기능 설정
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}")
try:
await popup_page.wait_for_load_state()
print("새 창 로드 완료")
except Exception as e:
print(f"새 창 로드 중 에러 발생: {e}")
traceback.print_exc()
return
await popup_page.evaluate('(selector) => document.querySelector(selector).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('(selector) => document.querySelector(selector).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('(selector) => document.querySelector(selector).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:
# 2단계 인증 처리
popup_page = await self.handle_two_factor_authentication(popup_page, market_api_keys)
# 팝업 페이지가 닫혀 있는지 확인
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) # 잠시 대기
await asyncio.sleep(2) # 잠시 대기
# 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
async def handle_two_factor_authentication(self, popup_page, market_api_keys):
try:
print("2단계 인증 발생 확인 중...")
try:
await popup_page.wait_for_load_state()
# 2단계 인증 페이지가 있는지 확인
two_factor_element = await popup_page.wait_for_selector("div#root .Layout_layout__2COMk .Layout_inner__3QZZI .TwoStepCertify_h_title__1XYdA", timeout=2000)
# print(f"two_factor_element 존재 : {two_factor_element}")
two_factor_text = await two_factor_element.inner_text()
print(f"two_factor_text : [{two_factor_text}]")
print("2단계 인증 필요. 사용자에게 인증 번호 입력을 요청합니다.")
# PyQt5 다이얼로그를 통해 사용자 입력 받기
self.dialog.popup_page = popup_page
await self.dialog.show_dialog()
while self.dialog.isVisible():
await asyncio.sleep(1)
if not self.dialog.auth_method or not self.dialog.code:
print("사용자가 인증을 완료하지 않았습니다.")
return popup_page
# 페이지 다시 로드 및 상태 확인
await popup_page.wait_for_load_state()
print("2단계 인증 완료 후 페이지 리로드")
# 팝업 페이지가 닫혀 있는지 확인
if popup_page.is_closed():
print("팝업 페이지가 닫혔습니다. 로그인 성공으로 간주합니다.")
return popup_page
# 로그인 성공 확인
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 TargetClosedError:
print("팝업 페이지가 닫혔습니다. 로그인 성공으로 간주합니다.")
return popup_page
except TimeoutError:
print("2단계 인증 페이지 로드 시간 초과. 2단계 인증이 발생하지 않았습니다.")
return popup_page
except Exception as e:
print(f"2단계 인증 처리 중 에러 발생: {e}")
traceback.print_exc()
return popup_page
def generate_selector(self, tab_id, class_name, profile_index, state_class):
return f"#rc-tabs-0-panel-{tab_id} > div > div.{class_name} > div.ant-list.ant-list-vertical.ant-list-split.ant-list-bordered.css-1li46mu > div > div > ul > li:nth-child({profile_index}) > div > div:nth-child(2) > button.ant-btn.css-1li46mu.ant-btn-text.Body3Regular14.{state_class}"
def generate_profile_selector(self, tab_id, class_name, profile_index):
return f"#rc-tabs-0-panel-{tab_id} > div > div.{class_name} > div.ant-list.ant-list-vertical.ant-list-split.ant-list-bordered.css-1li46mu > div > div > ul > li:nth-child({profile_index}) > div > div:nth-child(1) > div.H5Medium16.CharacterPrimary85"
async def select_shipping_profile(self, market, market_api_keys):
profile_number = market_api_keys.get('배송프로필번호', '1')
profile_number = int(profile_number)
print(f"profile_number : {profile_number}")
try:
market_config = self.market_shipping_profile_selectors.get(market)
if not market_config:
# self.logger.error(f"{market} 마켓의 배송프로필 선택자는 설정되지 않았습니다.")
print(f"{market} 마켓의 배송프로필 선택자는 설정되지 않았습니다.")
return
tab_id, class_name = market_config
if profile_number < 1 or profile_number > 3:
# self.logger.warning(f"{market} 마켓에 유효하지 않은 배송프로필 번호가 선택되었습니다. 기본으로 1번 프로필을 사용합니다.")
print(f"{market} 마켓에 유효하지 않은 배송프로필 번호가 선택되었습니다. 기본으로 1번 프로필을 사용합니다.")
profile_number = 1
profile_selector = self.generate_profile_selector(tab_id, class_name, profile_number)
default_selected_selector = self.generate_selector(tab_id, class_name, profile_number, "CharacterDisabledPlaceholder25")
default_selectable_selector = self.generate_selector(tab_id, class_name, profile_number, "Primary6")
# profile_selector가 있는지 확인
is_profile_selector = await self.page.query_selector(profile_selector)
if not is_profile_selector:
# self.logger.warning(f"{market} 마켓에 배송프로필이 존재하지 않습니다. 기본으로 1번 프로필을 사용합니다.")
print(f"{market} 마켓에 배송프로필이 존재하지 않습니다. 기본으로 1번 프로필을 사용합니다.")
profile_selector = self.generate_profile_selector(tab_id, class_name, 1)
default_selected_selector = self.generate_selector(tab_id, class_name, 1, "CharacterDisabledPlaceholder25")
default_selectable_selector = self.generate_selector(tab_id, class_name, 1, "Primary6")
# 기본 프로필로 선택된 상태인지 확인
is_default_selected = await self.page.query_selector(default_selected_selector)
if is_default_selected:
# self.logger.info(f"{market} 마켓의 {profile_number}번 프로필이 이미 기본 프로필로 선택되어 있습니다.")
print(f"{market} 마켓의 {profile_number}번 프로필이 이미 기본 프로필로 선택되어 있습니다.")
return
# 기본 프로필로 선택 가능한 상태인지 확인
is_default_selectable = await self.page.query_selector(default_selectable_selector)
if is_default_selectable:
await is_default_selectable.click()
# self.logger.info(f"{market} 마켓의 {profile_number}번 프로필을 기본 프로필로 선택했습니다.")
print(f"{market} 마켓의 {profile_number}번 프로필을 기본 프로필로 선택했습니다.")
else:
# self.logger.error(f"{market} 마켓의 {profile_number}번 프로필을 기본 프로필로 선택할 수 없습니다.")
print(f"{market} 마켓의 {profile_number}번 프로필을 기본 프로필로 선택할 수 없습니다.")
except Exception as e:
# self.logger.error(f"{market} 배송프로필 선택 중 오류 발생: {e}")
print(f"{market} 배송프로필 선택 중 오류 발생: {e}")
traceback.print_exc()