스레드 통합중
1620
browser_control.py
|
|
@ -0,0 +1,976 @@
|
|||
from playwright.async_api import async_playwright, TimeoutError
|
||||
from PySide6.QtCore import QThread, Signal
|
||||
import re
|
||||
import pyautogui
|
||||
import time
|
||||
import win32gui, win32con
|
||||
from bs4 import BeautifulSoup
|
||||
import asyncio
|
||||
import os, sys, random
|
||||
import requests
|
||||
from PIL import Image
|
||||
from io import BytesIO
|
||||
|
||||
class BrowserController(QThread):
|
||||
data_collected = Signal(bool, str)
|
||||
|
||||
def __init__(self, app, logger, locator_manager, login_infos, toggle_states):
|
||||
super().__init__()
|
||||
self.logger = logger
|
||||
self.log_files = ["appTranslator.log", "appTranslator.log.1", "appTranslator.log.2", "appTranslator.log.3", "appTranslator.log.4", "appTranslator.log.5"]
|
||||
self.locator_manager = locator_manager
|
||||
self.toggle_states = toggle_states
|
||||
self.login_infos = login_infos
|
||||
self.chrome_hwnd = None
|
||||
self.whale_hwnd = None
|
||||
|
||||
self.whale_browser = None # 필요한 경우 whale_browser 객체를 설정
|
||||
self.playwright = None
|
||||
self.browser = None
|
||||
self.page = None
|
||||
|
||||
|
||||
# BrowserController에 해당하는 모든 locator를 정의
|
||||
self.chrome_window_name = self.locator_manager.get_locator('BrowserControl', 'chrome_window_name')
|
||||
self.login_email_locator = self.locator_manager.get_locator('BrowserControl', 'login_email_locator')
|
||||
self.login_password_locator = self.locator_manager.get_locator('BrowserControl', 'login_password_locator')
|
||||
self.login_button_locator = self.locator_manager.get_locator('BrowserControl', 'login_button_locator')
|
||||
self.admin_toggle_locator = self.locator_manager.get_locator('BrowserControl', 'admin_toggle_locator')
|
||||
self.staff_id_locator = self.locator_manager.get_locator('BrowserControl', 'staff_id_locator')
|
||||
self.staff_login_button_locator = self.locator_manager.get_locator('BrowserControl', 'staff_login_button_locator')
|
||||
self.close_ad_dialog_locator = self.locator_manager.get_locator('BrowserControl', 'close_ad_dialog_locator')
|
||||
self.close_ad_button_locator = self.locator_manager.get_locator('BrowserControl', 'close_ad_button_locator')
|
||||
self.total_product_count_locator = self.locator_manager.get_locator('BrowserControl', 'total_product_count_locator')
|
||||
self.total_product_count_for_registed_locator = self.locator_manager.get_locator('BrowserControl', 'total_product_count_for_registed_locator')
|
||||
self.product_parent_locator= self.locator_manager.get_locator('BrowserControl', 'product_parent_locator')
|
||||
self.product_name_inner_locator = self.locator_manager.get_locator('BrowserControl', 'product_name_inner_locator')
|
||||
self.product_price_inner_locator = self.locator_manager.get_locator('BrowserControl', 'product_price_inner_locator')
|
||||
self.product_image_inner_locator = self.locator_manager.get_locator('BrowserControl', 'product_image_inner_locator')
|
||||
self.product_name_for_ed_template = self.locator_manager.get_locator('BrowserControl', 'product_name_for_ed_template')
|
||||
self.product_price_for_ed_template = self.locator_manager.get_locator('BrowserControl', 'product_price_for_ed_template')
|
||||
self.product_image_for_ed_template = self.locator_manager.get_locator('BrowserControl', 'product_image_for_ed_template')
|
||||
self.product_edit_button_template = self.locator_manager.get_locator('BrowserControl', 'product_edit_button_template')
|
||||
self.current_page = self.locator_manager.get_locator('BrowserControl', 'current_page')
|
||||
self.next_page_button_template = self.locator_manager.get_locator('BrowserControl', 'next_page_button_template')
|
||||
self.new_product_page_locator = self.locator_manager.get_locator('BrowserControl', 'new_product_page_locator')
|
||||
self.registered_product_page_locator = self.locator_manager.get_locator('BrowserControl', 'registered_product_page_locator')
|
||||
self.current_page_locator = self.locator_manager.get_locator('BrowserControl', 'current_page_locator')
|
||||
self.source_button_locator = self.locator_manager.get_locator('BrowserControl', 'source_button_locator')
|
||||
self.ck_source_editing_area_locator = self.locator_manager.get_locator('BrowserControl', 'ck_source_editing_area_locator')
|
||||
self.option_input_field_locator = self.locator_manager.get_locator('BrowserControl', 'option_input_field_locator')
|
||||
self.title_tab_locator = self.locator_manager.get_locator('BrowserControl', 'title_tab_locator')
|
||||
self.option_tab_locator = self.locator_manager.get_locator('BrowserControl', 'option_tab_locator')
|
||||
self.price_tab_locator = self.locator_manager.get_locator('BrowserControl', 'price_tab_locator')
|
||||
self.tag_tab_locator = self.locator_manager.get_locator('BrowserControl', 'tag_tab_locator')
|
||||
self.thumb_tab_locator = self.locator_manager.get_locator('BrowserControl', 'thumb_tab_locator')
|
||||
self.detail_tab_locator = self.locator_manager.get_locator('BrowserControl', 'detail_tab_locator')
|
||||
self.upload_tab_locator = self.locator_manager.get_locator('BrowserControl', 'upload_tab_locator')
|
||||
self.save_button_locator = self.locator_manager.get_locator('BrowserControl', 'save_button_locator')
|
||||
|
||||
self.text_templates = self.locator_manager.selectors.get('DetailPageTextTemplates', {})
|
||||
|
||||
|
||||
# # 스레드 종료 시 close_whale_window_if_exists 호출
|
||||
# self.finished.connect(self.cleanup)
|
||||
|
||||
def get_page(self):
|
||||
return self.page
|
||||
|
||||
async def start_browser(self):
|
||||
"""크롬 브라우저 실행 및 페이지 로딩"""
|
||||
self.logger.debug('크롬 브라우저 실행 중...')
|
||||
|
||||
# Playwright를 수동으로 실행하여 브라우저 유지
|
||||
self.playwright = await async_playwright().start()
|
||||
|
||||
# cx_Freeze로 패키징된 경우와 일반 Python 실행 환경 구분하여 경로 설정
|
||||
if getattr(sys, 'frozen', False):
|
||||
browser_path = os.path.join(os.path.dirname(sys.executable), 'browsers', 'chromium-1112', 'chrome-win','chrome.exe')
|
||||
extension_path = os.path.join(os.path.dirname(sys.executable), 'browsers', 'extensions', '1.1.100_0')
|
||||
user_data_dir = os.path.join(os.path.dirname(sys.executable), 'browsers', 'user_data')
|
||||
else:
|
||||
browser_path = os.path.join(os.path.dirname(__file__), 'browsers', 'chromium-1112', 'chrome-win','chrome.exe')
|
||||
extension_path = os.path.join(os.path.dirname(__file__), 'browsers', 'extensions', '1.1.100_0')
|
||||
user_data_dir = os.path.join(os.path.dirname(__file__), 'browsers', 'user_data')
|
||||
|
||||
self.logger.debug(f"브라우저 경로: {browser_path}")
|
||||
self.logger.debug(f"확장 프로그램 경로: {extension_path}")
|
||||
self.logger.debug(f"사용자 폴더 경로: {user_data_dir}")
|
||||
|
||||
# 사용자 데이터 디렉토리가 존재하지 않으면 생성
|
||||
if not os.path.exists(user_data_dir):
|
||||
os.makedirs(user_data_dir)
|
||||
self.logger.debug(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.logger.debug(f"user_agent: {user_agent}")
|
||||
|
||||
# 브라우저 시작 및 설정
|
||||
self.browser = await self.playwright.chromium.launch_persistent_context(
|
||||
user_data_dir,
|
||||
headless=False,
|
||||
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}',
|
||||
'--start-maximized',
|
||||
'--window-size=1920,1080'
|
||||
],
|
||||
executable_path=browser_path,
|
||||
user_agent=user_agent
|
||||
)
|
||||
|
||||
# 기본 페이지가 없을 수 있으므로 새로운 페이지 생성
|
||||
self.page = await self.browser.new_page()
|
||||
self.logger.info('새 페이지 로딩 중...')
|
||||
|
||||
await self.page.goto('https://percenty.co.kr/signin')
|
||||
self.logger.info('percenty.co.kr/signin 로딩 완료')
|
||||
|
||||
# 첫 번째 기본 탭 닫기
|
||||
if self.browser.pages:
|
||||
await self.browser.pages[0].close()
|
||||
|
||||
# 페이지 제목을 가져와서 창 제목으로 활용
|
||||
page_title = await 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.warning('크롬 창을 찾을 수 없습니다.')
|
||||
else:
|
||||
self.logger.debug(f'크롬 창 핸들: {self.chrome_hwnd}')
|
||||
|
||||
await self.login()
|
||||
await self.close_ad_if_exists()
|
||||
|
||||
if self.toggle_states['ed_mode']:
|
||||
await self.go_to_registered_product_page()
|
||||
self.logger.info('등록 상품 관리 페이지로 이동 중...')
|
||||
else:
|
||||
self.logger.info('신규 상품 등록 페이지로 이동 중...')
|
||||
await self.go_to_new_product_page()
|
||||
|
||||
async def login(self):
|
||||
"""로그인 처리"""
|
||||
is_admin = self.login_infos['is_admin']
|
||||
self.logger.info(f'로그인 시도 중: {"관리자" if is_admin else "직원"} 계정')
|
||||
|
||||
if is_admin:
|
||||
# 관리자 로그인 처리
|
||||
await self.page.fill(self.login_email_locator, self.login_infos['admin_id'])
|
||||
await self.page.fill(self.login_password_locator, self.login_infos['admin_pw'])
|
||||
await self.page.click(self.login_button_locator)
|
||||
else:
|
||||
# 관리자 토글 버튼을 클릭해서 직원 로그인 화면 활성화
|
||||
admin_toggle = self.page.locator(self.admin_toggle_locator)
|
||||
if await admin_toggle.get_attribute("aria-checked") == "true":
|
||||
await admin_toggle.click() # 관리자 모드에서 직원 모드로 전환
|
||||
|
||||
await self.page.fill(self.login_email_locator, self.login_infos['admin_id'])
|
||||
await self.page.fill(self.staff_id_locator, self.login_infos['user_id'])
|
||||
await self.page.fill(self.login_password_locator, self.login_infos['user_pw'])
|
||||
await self.page.click(self.staff_login_button_locator)
|
||||
|
||||
self.logger.info(f'로그인 완료: {"관리자" if is_admin else "직원"} 계정')
|
||||
|
||||
# await self.page.wait_for_load_state('networkidle', timeout=10000)
|
||||
|
||||
async def close_browser(self):
|
||||
"""브라우저 종료"""
|
||||
if self.browser:
|
||||
await self.browser.close()
|
||||
await self.playwright.stop()
|
||||
self.cleanup()
|
||||
self.logger.info('브라우저 종료됨.')
|
||||
|
||||
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.error('크롬 창을 찾을 수 없습니다.')
|
||||
|
||||
|
||||
async def get_total_product_count_ori(self):
|
||||
try:
|
||||
# JavaScript로 해당 요소의 텍스트를 가져옴
|
||||
element_text = await 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
|
||||
|
||||
|
||||
async def get_total_product_count(self):
|
||||
total_count = 0
|
||||
items_per_page = 0
|
||||
|
||||
try:
|
||||
# total_count_elements = await self.page.query_selector_all(".sc-dOvA-dm.jqRNYf")
|
||||
total_count_element = await self.page.query_selector("div#root span:has-text('개 상품')")
|
||||
items_per_page_element = await self.page.query_selector("div#root [title$='개씩 보기']")
|
||||
|
||||
self.logger.debug(f"total_count_element : {total_count_element}")
|
||||
|
||||
if total_count_element:
|
||||
total_count_text = await total_count_element.inner_text()
|
||||
if "총" in total_count_text and "개 상품" in total_count_text:
|
||||
total_count = int(''.join(re.findall(r'\d+', total_count_text)))
|
||||
self.logger.info(f"총 상품수 확인: {total_count} 개")
|
||||
|
||||
# 페이지당 상품 수 추출
|
||||
if items_per_page_element:
|
||||
items_per_page_text = await items_per_page_element.get_attribute("title")
|
||||
if items_per_page_text and "개씩 보기" in items_per_page_text:
|
||||
items_per_page = int(''.join(re.findall(r'\d+', items_per_page_text)))
|
||||
self.logger.info(f"페이지당 상품수 확인: {items_per_page} 개씩 보기")
|
||||
|
||||
# 결과 반환
|
||||
if total_count:
|
||||
return {"total_count": total_count, "items_per_page": items_per_page}
|
||||
|
||||
return {"total_count": 0, "items_per_page": 0}
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"상품 수를 가져오는 중 오류 발생: {e}", exc_info=True)
|
||||
return {"total_count": 0, "items_per_page": 0}
|
||||
|
||||
|
||||
# def fetch_image_urls_ori(self, html_content):
|
||||
# """
|
||||
# HTML 콘텐츠에서 모든 <img> 태그의 URL을 순서대로 추출하고 중복 제거.
|
||||
# """
|
||||
# soup = BeautifulSoup(html_content, 'html.parser')
|
||||
|
||||
# # 순서를 유지하면서 중복을 제거하기 위해 리스트 사용
|
||||
# image_urls = []
|
||||
# seen_urls = set()
|
||||
|
||||
# # <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:
|
||||
# url = img_tag['src']
|
||||
# if url not in seen_urls:
|
||||
# image_urls.append(url)
|
||||
# seen_urls.add(url) # 중복 방지
|
||||
|
||||
# # 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:
|
||||
# url = img['src']
|
||||
# if url not in seen_urls:
|
||||
# image_urls.append(url)
|
||||
# seen_urls.add(url) # 중복 방지
|
||||
|
||||
# return image_urls
|
||||
|
||||
def fetch_image_urls(self, html_content):
|
||||
"""
|
||||
HTML 콘텐츠에서 모든 <img> 태그의 URL을 추출하는 함수.
|
||||
<figure> 안의 <img> 태그와 독립된 <img> 태그 모두 처리.
|
||||
"""
|
||||
soup = BeautifulSoup(html_content, 'html.parser')
|
||||
|
||||
# 모든 <img> 태그를 찾기
|
||||
image_urls = []
|
||||
img_tags = soup.find_all('img')
|
||||
|
||||
for img in img_tags:
|
||||
# img 태그에서 src 속성 추출
|
||||
if 'src' in img.attrs:
|
||||
image_url = img['src']
|
||||
image_urls.append(image_url)
|
||||
self.logger.debug(f"fetch_image_urls 에서 추출한 이미지URL 갯수 : {len(image_urls)} 개")
|
||||
|
||||
self.logger.debug(f"fetch_image_urls 에서 추출한 이미지URL 목록 : {image_urls}")
|
||||
|
||||
return image_urls
|
||||
|
||||
async def close_ad_if_exists(self):
|
||||
"""광고 다이얼로그가 있으면 닫기 버튼을 클릭하는 메서드"""
|
||||
try:
|
||||
# 광고 다이얼로그가 나타날 때까지 기다림
|
||||
await self.page.wait_for_selector(self.close_ad_dialog_locator, timeout=5000, state='visible')
|
||||
self.logger.info("다이얼로그가 발견되었습니다. 닫기 버튼을 클릭합니다.")
|
||||
|
||||
# 닫기 버튼 클릭
|
||||
close_button = await self.page.query_selector(self.close_ad_button_locator)
|
||||
if close_button:
|
||||
await close_button.click()
|
||||
self.logger.info("다이얼로그를 성공적으로 닫았습니다.")
|
||||
else:
|
||||
self.logger.warning("닫기 버튼을 찾지 못했습니다.")
|
||||
|
||||
except TimeoutError:
|
||||
# 다이얼로그가 없을 때: info 수준의 로그로 기록
|
||||
self.logger.info("다이얼로그가 발견되지 않았습니다. 타임아웃이 발생했습니다.")
|
||||
except Exception as e:
|
||||
# 다른 예외 상황 발생 시 error로 기록
|
||||
self.logger.error(f"다이얼로그 닫기 중 오류 발생: {e}", exc_info=True)
|
||||
|
||||
async def go_to_new_product_page(self):
|
||||
"""신규 상품 등록 페이지로 이동"""
|
||||
try:
|
||||
new_product_page_locator = self.locator_manager.get_locator('BrowserControl', 'new_product_page_locator')
|
||||
await self.page.click(new_product_page_locator)
|
||||
self.logger.info("신규 상품 등록 페이지로 이동 완료.")
|
||||
except Exception as e:
|
||||
self.logger.error(f"신규 상품 등록 페이지 이동 중 오류: {e}", exc_info=True)
|
||||
|
||||
|
||||
async def go_to_registered_product_page(self):
|
||||
"""신규 상품 등록 페이지로 이동"""
|
||||
try:
|
||||
registered_product_page_locator = self.locator_manager.get_locator('BrowserControl', 'registered_product_page_locator')
|
||||
await self.page.click(registered_product_page_locator)
|
||||
self.logger.info("등록 상품 관리 페이지로 이동 완료.")
|
||||
except Exception as e:
|
||||
self.logger.error(f"등록 상품 관리 페이지 이동 중 오류: {e}", exc_info=True)
|
||||
|
||||
|
||||
# async def get_product_edit_buttons(self):
|
||||
# """현재 페이지의 세부사항 수정 및 업로드 버튼을 찾기"""
|
||||
# try:
|
||||
# # 버튼 선택자를 가져옴
|
||||
# edit_button_selector = self.product_edit_button
|
||||
|
||||
# if not edit_button_selector:
|
||||
# self.logger.warning("상품 수정 버튼의 선택자를 찾을 수 없습니다.")
|
||||
# return []
|
||||
|
||||
# # 선택자를 사용해 버튼 객체를 찾음
|
||||
# buttons = self.page.locator(edit_button_selector)
|
||||
|
||||
# # 버튼이 존재하는지 확인
|
||||
# if await buttons.count() == 0:
|
||||
# self.logger.warning("세부사항 수정 및 업로드 버튼을 찾을 수 없습니다.")
|
||||
# return []
|
||||
|
||||
# count = await buttons.count()
|
||||
# self.logger.info(f"수정할 상품 개수: {count}")
|
||||
|
||||
# # 모든 버튼을 리스트로 반환
|
||||
# return [buttons.nth(i) for i in range(count)]
|
||||
|
||||
# except Exception as e:
|
||||
# self.logger.error(f"상품 수정 버튼을 찾는 중 오류: {e}", exc_info=True)
|
||||
# return []
|
||||
|
||||
async def is_button_disabled(self, button):
|
||||
"""버튼이 disabled 상태인지 확인"""
|
||||
try:
|
||||
# 버튼의 disabled 속성 확인
|
||||
is_disabled = await button.get_attribute('disabled')
|
||||
return is_disabled is not None # disabled 속성이 있으면 True 반환
|
||||
except Exception as e:
|
||||
self.logger.error(f"상품 수정 버튼 상태 확인 중 오류 발생: {e}", exc_info=True)
|
||||
return False # 오류 발생 시 기본적으로 활성화된 것으로 처리
|
||||
|
||||
async def get_product_edit_buttons_by_templete(self):
|
||||
"""현재 페이지의 세부사항 수정 및 업로드 버튼을 찾기"""
|
||||
try:
|
||||
# 버튼 선택자 설정
|
||||
# edit_button_selector_template = f'//button[span[text()="세부사항 수정 및 업로드"]]'
|
||||
self.product_edit_button_template
|
||||
# 선택자를 사용해 버튼 객체를 찾음
|
||||
buttons = self.page.locator(self.product_edit_button_template)
|
||||
|
||||
# 버튼이 존재하는지 확인
|
||||
button_count = await buttons.count()
|
||||
if button_count == 0:
|
||||
self.logger.warning("세부사항 수정 및 업로드 버튼을 찾을 수 없습니다.")
|
||||
return []
|
||||
|
||||
self.logger.info(f"현재 페이지의 수정할 상품 개수: {button_count}")
|
||||
|
||||
# 모든 버튼을 리스트로 반환
|
||||
return [buttons.nth(i) for i in range(button_count)]
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"상품 수정 버튼을 찾는 중 오류: {e}", exc_info=True)
|
||||
return []
|
||||
|
||||
|
||||
async def click_modify_button_by_text(self, index):
|
||||
"""인덱스에 해당하는 '세부사항 수정 및 업로드' 버튼 클릭"""
|
||||
try:
|
||||
# config.ini에서 선택자 가져오기
|
||||
button_template = self.locator_manager.get_locator('BrowserControl', 'product_edit_button_template')
|
||||
button_selector = f'({button_template})[{index}]'
|
||||
|
||||
button = await self.page.query_selector(button_selector)
|
||||
|
||||
# 버튼이 화면에 보이도록 스크롤 후 클릭
|
||||
if button:
|
||||
await button.scroll_into_view_if_needed()
|
||||
await self.page.evaluate('arguments[0].click();', button)
|
||||
self.logger.info(f'{index}번째 상품의 수정 버튼 클릭 완료')
|
||||
else:
|
||||
self.logger.warning(f'{index}번째 상품의 수정 버튼을 찾지 못했습니다.')
|
||||
except Exception as e:
|
||||
self.logger.error(f'{index}번째 상품의 수정 버튼 클릭 중 오류: {str(e)}')
|
||||
|
||||
|
||||
async def open_product_edit_dialog(self, button):
|
||||
"""상품 수정 다이얼로그 열기"""
|
||||
try:
|
||||
# 요소가 화면에 없을 경우 스크롤하여 보이도록 함
|
||||
await button.scroll_into_view_if_needed()
|
||||
self.logger.debug("상품의 '세부사항 수정 및 업로드' 버튼을 화면에 보이도록 스크롤.")
|
||||
|
||||
await button.click()
|
||||
self.logger.info("세부사항 수정 다이얼로그 열기 완료.")
|
||||
await self.page.wait_for_selector('div.ant-tabs-nav') # 다이얼로그가 완전히 로딩될 때까지 기다림
|
||||
except Exception as e:
|
||||
self.logger.error(f"세부사항 수정 다이얼로그 열기 중 오류: {e}", exc_info=True)
|
||||
|
||||
async def click_detail_tab(self):
|
||||
"""상세페이지 탭 클릭"""
|
||||
try:
|
||||
await self.page.click(self.detail_tab_locator)
|
||||
self.logger.info("상세페이지 탭 클릭 완료.")
|
||||
except Exception as e:
|
||||
self.logger.error(f"상세페이지 탭 클릭 중 오류: {e}", exc_info=True)
|
||||
|
||||
async def click_option_tab(self):
|
||||
"""옵션 탭 클릭"""
|
||||
try:
|
||||
await self.page.click(self.option_tab_locator)
|
||||
self.logger.info("옵션 탭 클릭 완료.")
|
||||
except Exception as e:
|
||||
self.logger.error(f"옵션 탭 클릭 중 오류: {e}", exc_info=True)
|
||||
|
||||
async def click_price_tab(self):
|
||||
"""가격 탭 클릭"""
|
||||
try:
|
||||
await self.page.click(self.price_tab_locator)
|
||||
self.logger.info("가격 탭 클릭 완료.")
|
||||
except Exception as e:
|
||||
self.logger.error(f"가격 탭 클릭 중 오류: {e}", exc_info=True)
|
||||
|
||||
async def click_title_tab(self):
|
||||
"""상품명 탭 클릭"""
|
||||
try:
|
||||
await self.page.click(self.title_tab_locator)
|
||||
self.logger.info("상품명 탭 클릭 완료.")
|
||||
except Exception as e:
|
||||
self.logger.error(f"상품명 탭 클릭 중 오류: {e}", exc_info=True)
|
||||
|
||||
def generate_restored_html(self, urls):
|
||||
"""이미지 URL 목록을 HTML 형식으로 변환하는 메서드"""
|
||||
html_content = '<p> </p>'
|
||||
for url in urls:
|
||||
html_content += f'<figure class="image"><img src="{url}" style="aspect-ratio:1/1;"></figure>\n'
|
||||
return html_content
|
||||
|
||||
def deleted_img_urls_from_logs(self):
|
||||
"""로그 파일에서 상품명과 이미지 URL 목록을 추출하여 딕셔너리로 반환하는 메서드"""
|
||||
image_data = {}
|
||||
log_dir = os.path.join(os.path.dirname(__file__), "recovery_log")
|
||||
|
||||
# 로그 파일에서 필요한 정보만 추출
|
||||
for log_file in self.log_files:
|
||||
log_path = os.path.join(log_dir, log_file)
|
||||
if os.path.exists(log_path):
|
||||
with open(log_path, 'r', encoding='utf-8') as f:
|
||||
lines = f.readlines()
|
||||
|
||||
current_product = None
|
||||
for line in lines:
|
||||
# 상품명 추출
|
||||
product_match = re.search(r"원본 상품명 '(.+?)'", line)
|
||||
if product_match:
|
||||
current_product = product_match.group(1)
|
||||
image_data[current_product] = []
|
||||
|
||||
# 이미지 URL 목록 추출
|
||||
url_match = re.search(r"fetch_image_urls 에서 추출한 이미지URL 목록 : \[(.+?)\]", line)
|
||||
if url_match and current_product:
|
||||
# 각 URL에서 불필요한 작은따옴표 제거
|
||||
urls = [url.strip("'\"") for url in url_match.group(1).split(", ")]
|
||||
image_data[current_product].extend(urls)
|
||||
current_product = None # Reset after each product's URL extraction
|
||||
|
||||
self.logger.debug(f"복구된 이미지 URL 데이터: {image_data}")
|
||||
return image_data
|
||||
|
||||
async def recovery_image_urls(self, product_name, deleted_img_urls):
|
||||
"""상품명과 삭제된 이미지 URL 데이터를 이용해 복구 작업을 수행하는 메서드"""
|
||||
self.logger.debug("상품명과 삭제된 이미지 URL 데이터를 이용해 복구 작업을 수행하는 메서드")
|
||||
|
||||
if product_name in deleted_img_urls:
|
||||
# 소스 편집 모드로 전환
|
||||
source_button_locator = self.locator_manager.get_locator('BrowserControl', 'source_button_locator')
|
||||
ck_source_editing_area_locator = self.locator_manager.get_locator('BrowserControl', 'ck_source_editing_area_locator')
|
||||
await self.page.click(source_button_locator)
|
||||
self.logger.debug("recovery_image_urls : 소스 버튼 클릭 완료.")
|
||||
|
||||
# 기존 extract_image_urls와 유사하게 HTML 소스를 가져옴
|
||||
textarea = await self.page.wait_for_selector(ck_source_editing_area_locator, timeout=5000)
|
||||
data_value = await textarea.get_attribute("data-value")
|
||||
|
||||
# HTML 소스에서 이미지 URL 추출
|
||||
image_urls = self.fetch_image_urls(data_value)
|
||||
self.logger.info(f'recovery_image_urls추출된 이미지 URL 수: {len(image_urls)}')
|
||||
|
||||
# 이미지 태그가 없으면 로그에서 추출한 데이터를 HTML로 복원하여 입력
|
||||
if len(image_urls) == 0:
|
||||
restored_html = self.generate_restored_html(deleted_img_urls[product_name])
|
||||
await self.page.evaluate(f'document.querySelector("{ck_source_editing_area_locator}").setAttribute("data-value", `{restored_html}`)')
|
||||
self.logger.debug("recovery_image_urls로그 데이터를 이용하여 HTML 복원 완료.")
|
||||
else:
|
||||
self.logger.debug("이미 이미지가 있으므로 복원 작업을 패스합니다.")
|
||||
|
||||
# 소스 편집 모드 종료
|
||||
await self.page.click(source_button_locator)
|
||||
self.logger.debug('소스 버튼 재 클릭 완료.')
|
||||
else:
|
||||
self.logger.debug(f"로그에 해당 상품명 '{product_name}'에 대한 데이터가 존재하지 않습니다.")
|
||||
|
||||
def generate_restored_html(self, urls):
|
||||
"""이미지 URL 목록을 HTML 형식으로 변환하는 메서드, 각 이미지의 가로세로 비율 추가"""
|
||||
html_content = '<p> </p>\n'
|
||||
for url in urls:
|
||||
width, height = self.get_image_size(url)
|
||||
aspect_ratio = f"{width}/{height}" if width and height else "1/1"
|
||||
if width and height:
|
||||
html_content += (
|
||||
f'<figure class="image">'
|
||||
f'<img style="aspect-ratio:{aspect_ratio};" src="{url}" width="{width}" height="{height}">'
|
||||
f'</figure>\n'
|
||||
)
|
||||
else:
|
||||
# 이미지 크기를 확인할 수 없을 경우 기본 형식으로 추가
|
||||
html_content += f'<figure class="image"><img src="{url}"></figure>\n'
|
||||
return html_content
|
||||
|
||||
def get_image_size(self, url):
|
||||
"""이미지 URL로부터 가로와 세로 크기를 가져오는 메서드"""
|
||||
try:
|
||||
# URL에서 불필요한 따옴표 제거
|
||||
cleaned_url = url.strip("'\"")
|
||||
|
||||
response = requests.get(cleaned_url, timeout=5)
|
||||
response.raise_for_status()
|
||||
image = Image.open(BytesIO(response.content))
|
||||
return image.width, image.height
|
||||
except Exception as e:
|
||||
self.logger.warning(f"이미지 크기 확인 실패 - {cleaned_url}: {e}")
|
||||
return None, None
|
||||
|
||||
|
||||
async def extract_image_urls(self, optionHandler, is_option_data=False):
|
||||
"""상세페이지에서 이미지 URL 추출"""
|
||||
try:
|
||||
# 소스 편집 모드로 전환
|
||||
source_button_locator = self.locator_manager.get_locator('BrowserControl', 'source_button_locator')
|
||||
ck_source_editing_area_locator = self.locator_manager.get_locator('BrowserControl', 'ck_source_editing_area_locator')
|
||||
|
||||
# 소스 편집 모드로 전환
|
||||
await self.page.click(source_button_locator)
|
||||
self.logger.debug("소스 버튼 클릭 완료.")
|
||||
|
||||
|
||||
# 'data-value' 속성 값을 추출 (textarea 요소)
|
||||
textarea = await self.page.wait_for_selector(ck_source_editing_area_locator, timeout=5000)
|
||||
data_value = await textarea.get_attribute("data-value")
|
||||
|
||||
|
||||
# HTML 소스에서 이미지 URL 추출
|
||||
image_urls = self.fetch_image_urls(data_value)
|
||||
self.logger.info(f'추출된 이미지 URL 수: {len(image_urls)}')
|
||||
|
||||
# HTML 소스에서 이미지 URL 삭제
|
||||
self.logger.debug('img 태그를 삭제 중...')
|
||||
data_value_element = await self.page.query_selector(ck_source_editing_area_locator)
|
||||
new_value = ""
|
||||
if data_value_element:
|
||||
await self.page.evaluate(f'() => document.querySelector("{ck_source_editing_area_locator}").setAttribute("data-value", "{new_value}")')
|
||||
updated_value = await 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 삭제 후 다시 소스 버튼 클릭
|
||||
await self.page.click(source_button_locator)
|
||||
self.logger.debug('소스 버튼 재 클릭 완료.')
|
||||
|
||||
|
||||
if is_option_data:
|
||||
self.logger.debug('옵션 데이터 입력 시작')
|
||||
option_data = {} # option_data 초기화
|
||||
option_data = optionHandler.get_selected_translated_options()
|
||||
is_single = optionHandler.option_info['is_single_option']
|
||||
|
||||
is_single = True # 옵션입력 일단 제외
|
||||
self.logger.debug('옵션입력 일단 제외')
|
||||
|
||||
self.logger.debug('가져온 옵션 데이터')
|
||||
self.logger.debug(f'{option_data}')
|
||||
|
||||
# 옵션 입력 필드 선택
|
||||
input_field = await self.page.wait_for_selector(self.option_input_field_locator, timeout=5000)
|
||||
await input_field.press('Enter')
|
||||
|
||||
# 선두부 텍스트 입력
|
||||
for key in sorted(self.text_templates.keys()):
|
||||
leading_text = self.text_templates[key]
|
||||
if 'leading_text' in key and leading_text: # leading_text 항목만 가져오기
|
||||
await input_field.type(leading_text)
|
||||
await input_field.press('Enter')
|
||||
self.logger.info(f"{key} 텍스트 입력 완료: {leading_text}")
|
||||
|
||||
if not is_single:
|
||||
self.logger.info('단일옵션이 아니므로 옵션목록을 입력')
|
||||
|
||||
# 각 옵션을 한 줄씩 입력
|
||||
await input_field.type("# 옵션 목록")
|
||||
await input_field.press('Enter')
|
||||
|
||||
# 첫 번째 옵션의 번역된 옵션명만 입력
|
||||
first_key = list(option_data.keys())[0]
|
||||
first_value = option_data[first_key]
|
||||
await input_field.type(f"- 1. {first_value}")
|
||||
await input_field.press('Enter') # 첫 번째 옵션 이후 엔터로 줄바꿈
|
||||
|
||||
# 나머지 옵션도 번역된 옵션명만 입력
|
||||
for i, (key, value) in enumerate(list(option_data.items())[1:], start=2):
|
||||
await input_field.type(f"{i}. {value}") # 옵션 번호와 번역된 옵션명만 입력
|
||||
await input_field.press('Enter') # 엔터 키를 입력하여 줄바꿈
|
||||
|
||||
# 목록 끝을 알리기 위해 엔터 두 번 입력
|
||||
await input_field.press('Enter')
|
||||
await input_field.press('Enter')
|
||||
|
||||
# 후두부 텍스트 입력
|
||||
await input_field.type('### 나열된 옵션목록 이외의 옵션이 필요하실 경우 고객센터로 연락주세요.')
|
||||
await input_field.press('Enter')
|
||||
await input_field.type('---')
|
||||
await input_field.press('Enter')
|
||||
|
||||
self.logger.info('옵션 데이터 입력 완료')
|
||||
|
||||
return image_urls
|
||||
except Exception as e:
|
||||
self.logger.error(f"이미지 URL 추출 & 옵션데이터 입력 처리 중 오류: {e}", exc_info=True)
|
||||
return image_urls if image_urls else []
|
||||
|
||||
def paste_image_in_chrome(self, clipboardImageManager, url, is_success_translated, toggle_states, is_watermark=False, watermark_text= ""):
|
||||
"""크롬으로 포커스를 옮기고 클립보드의 이미지를 붙여넣고 엔터 입력"""
|
||||
self.logger.debug("크롬으로 포커스를 옮기고 클립보드의 이미지를 붙여넣고 엔터 입력")
|
||||
try:
|
||||
self.switch_to_chrome() # 크롬으로 포커스 이동
|
||||
clipboardImageManager.process_clipboard(original_url=url, is_success_translated=is_success_translated, toggle_states=toggle_states) # 클립보드 내용을 처리
|
||||
# clipboard_content = pyperclip.paste()
|
||||
if clipboardImageManager.is_clipboard_image():
|
||||
pyautogui.hotkey('ctrl', 'v') # 클립보드 이미지 붙여넣기
|
||||
self.logger.info("이미지 붙여넣기 완료.")
|
||||
pyautogui.press('right') # 오른쪽 입력
|
||||
self.logger.debug("이미지 붙여넣기 완료.")
|
||||
clipboardImageManager.clear_clipboard()
|
||||
self.logger.info("이미지 붙여넣기 완료로 클립보드 비우기.")
|
||||
return True
|
||||
else:
|
||||
self.logger.warning("클립보드가 비어있습니다.")
|
||||
return False
|
||||
except Exception as e:
|
||||
self.logger.error(f"이미지 붙여넣기 중 오류: {e}", exc_info=True)
|
||||
return False
|
||||
|
||||
async def save_and_ecs_product_edit(self):
|
||||
"""상품 수정 후 저장 버튼 클릭"""
|
||||
try:
|
||||
await self.page.click(self.save_button_locator)
|
||||
await self.page.keyboard.press("Escape")
|
||||
self.logger.info("상품 수정 내용 저장 및 ECS 완료.")
|
||||
except Exception as e:
|
||||
self.logger.error(f"저장 버튼 클릭 중 오류: {e}", exc_info=True)
|
||||
|
||||
async def save_product_edit(self):
|
||||
"""상품 수정 후 저장 버튼 클릭"""
|
||||
try:
|
||||
await self.page.click(self.save_button_locator)
|
||||
self.logger.info("상품 수정 내용 저장 완료.")
|
||||
except Exception as e:
|
||||
self.logger.error(f"저장 버튼 클릭 중 오류: {e}", exc_info=True)
|
||||
|
||||
async def go_to_next_page(self):
|
||||
"""다음 페이지로 이동"""
|
||||
try:
|
||||
# 현재 페이지가 몇 번째 페이지인지 확인 (클래스에 'ant-pagination-item-active'가 있는 요소)
|
||||
current_page = await self.page.query_selector(self.current_page_locator)
|
||||
|
||||
if not current_page:
|
||||
self.logger.warning("현재 페이지 정보를 찾을 수 없습니다.")
|
||||
return False
|
||||
|
||||
# 현재 활성화된 페이지 번호를 가져옴
|
||||
current_page_number = int(await current_page.get_attribute("title"))
|
||||
self.logger.info(f"현재페이지 : [{current_page_number}]")
|
||||
|
||||
next_page_number = current_page_number + 1
|
||||
|
||||
# 다음 페이지 버튼을 찾음 (title 속성으로 다음 페이지를 찾음)
|
||||
next_page_button_locator = self.next_page_button_template.format(page_number=next_page_number)
|
||||
next_page_button = await self.page.query_selector(next_page_button_locator)
|
||||
|
||||
if next_page_button:
|
||||
await next_page_button.click() # 페이지 버튼 클릭
|
||||
# await self.page.wait_for_load_state('domcontentloaded') # 페이지 로딩이 완료될 때까지 대기
|
||||
time.sleep(3)
|
||||
self.logger.info(f"페이지 {next_page_number}로 이동 완료.")
|
||||
return True
|
||||
else:
|
||||
self.logger.warning("다음 페이지가 없습니다.")
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(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('크롬 창으로 포커스 이동.')
|
||||
else:
|
||||
self.logger.warning('크롬 창을 찾을 수 없습니다.')
|
||||
except Exception as e:
|
||||
self.logger.error(f"크롬 포커스 전환 중 오류: {e}", exc_info=True)
|
||||
|
||||
async def scroll_with_wheel(self, direction="down", pause_time=0.5, max_scrolls=50):
|
||||
"""
|
||||
휠 스크롤을 사용하여 페이지를 위나 아래로 천천히 스크롤.
|
||||
|
||||
Parameters:
|
||||
- direction: 스크롤 방향 ("down"은 아래로, "up"은 위로).
|
||||
- pause_time: 스크롤 사이의 대기 시간 (초).
|
||||
- max_scrolls: 최대 스크롤 횟수.
|
||||
"""
|
||||
scroll_count = 0
|
||||
|
||||
self.logger.debug(f"스크롤 시작")
|
||||
|
||||
# 현재 페이지 높이 가져오기
|
||||
last_height = await self.page.evaluate("document.body.scrollHeight")
|
||||
self.logger.debug(f"현재 페이지 높이 가져오기 - {last_height}")
|
||||
|
||||
while scroll_count < max_scrolls:
|
||||
if direction == "down":
|
||||
# 아래로 스크롤
|
||||
self.logger.debug(f"scroll_count[{scroll_count}]회 : 휠 아래로 1000px")
|
||||
await self.page.evaluate("window.scrollBy(0, 1000);")
|
||||
elif direction == "up":
|
||||
# 위로 스크롤
|
||||
self.logger.debug(f"scroll_count[{scroll_count}]회 : 휠 위로 1000px")
|
||||
await self.page.evaluate("window.scrollBy(0, -1000);")
|
||||
else:
|
||||
raise ValueError("direction 인자는 'down' 또는 'up'만 허용됩니다.")
|
||||
|
||||
self.logger.debug(f"pause_time 슬립 : {pause_time}")
|
||||
await asyncio.sleep(pause_time)
|
||||
|
||||
# 새로운 페이지 높이 가져오기
|
||||
new_height = await self.page.evaluate("document.body.scrollHeight")
|
||||
self.logger.debug(f"새로운 페이지 높이 가져오기 - {new_height}")
|
||||
|
||||
# 스크롤이 더 이상 필요 없는 경우(페이지 끝에 도달)
|
||||
if direction == "down" and new_height == last_height:
|
||||
self.logger.debug(f"페이지 끝에 도달했습니다. 스크롤 횟수: {scroll_count}")
|
||||
break
|
||||
elif direction == "up" and new_height == 0:
|
||||
self.logger.debug(f"페이지 시작에 도달했습니다. 스크롤 횟수: {scroll_count}")
|
||||
break
|
||||
|
||||
self.logger.debug(f"새로운 페이지 높이를 현재높이로 재설정 - {new_height}")
|
||||
last_height = new_height
|
||||
scroll_count += 1
|
||||
self.logger.debug(f"스크롤 카운트 + 1")
|
||||
|
||||
if scroll_count == max_scrolls:
|
||||
self.logger.debug("최대 스크롤 횟수에 도달했습니다.")
|
||||
|
||||
async def scroll_with_keyboard(self, direction="down", pause_time=0.5, max_scrolls=50):
|
||||
"""
|
||||
키보드를 사용하여 페이지를 위나 아래로 천천히 스크롤.
|
||||
|
||||
Parameters:
|
||||
- direction: 스크롤 방향 ("down"은 아래로, "up"은 위로).
|
||||
- pause_time: 스크롤 사이의 대기 시간 (초).
|
||||
- max_scrolls: 최대 스크롤 횟수.
|
||||
"""
|
||||
scroll_count = 0
|
||||
|
||||
while scroll_count < max_scrolls:
|
||||
if direction == "down":
|
||||
# 아래로 스크롤 (Page Down 키 사용)
|
||||
await self.page.keyboard.press("PageDown")
|
||||
elif direction == "up":
|
||||
# 위로 스크롤 (Page Up 키 사용)
|
||||
await self.page.keyboard.press("PageUp")
|
||||
else:
|
||||
raise ValueError("direction 인자는 'down' 또는 'up'만 허용됩니다.")
|
||||
|
||||
await asyncio.sleep(pause_time)
|
||||
|
||||
scroll_count += 1
|
||||
|
||||
if scroll_count == max_scrolls:
|
||||
self.logger.debug("최대 스크롤 횟수에 도달했습니다.")
|
||||
|
||||
|
||||
async def collect_product_info(self, items_per_page, ed_mode):
|
||||
"""
|
||||
상품 정보를 수집하는 메서드
|
||||
"""
|
||||
try:
|
||||
product_infos = []
|
||||
product_name_elements = [] # product_name_element를 저장할 리스트
|
||||
|
||||
# ed_mode에 따라 product_elements 설정
|
||||
if ed_mode:
|
||||
# 각 상품의 이름, 가격, 이미지를 위한 선택자 리스트 구성 (index가 2부터 시작)
|
||||
product_elements = [
|
||||
{
|
||||
"name": self.product_name_for_ed_template.format(index=i),
|
||||
"price": self.product_price_for_ed_template.format(index=i),
|
||||
"image": self.product_image_for_ed_template.format(index=i)
|
||||
}
|
||||
for i in range(2, items_per_page + 2) # index가 2부터 시작하도록 설정
|
||||
]
|
||||
else:
|
||||
# ed_mode=False일 때는 각 상품의 부모 요소를 모두 선택
|
||||
product_elements = await self.page.query_selector_all(self.product_parent_locator)
|
||||
|
||||
for i, element in enumerate(product_elements[:items_per_page], start=1):
|
||||
try:
|
||||
if ed_mode:
|
||||
# ed_mode=True일 때는 각 상품의 개별 선택자 사용
|
||||
product_name_element = await self.page.wait_for_selector(element["name"], timeout=3000, state="attached")
|
||||
product_price_element = await self.page.wait_for_selector(element["price"], timeout=3000, state="attached")
|
||||
product_image_element = await self.page.wait_for_selector(element["image"], timeout=3000, state="attached")
|
||||
else:
|
||||
# ed_mode=False일 때 부모 요소 내의 선택자를 사용
|
||||
product_name_element = await self.page.wait_for_selector(self.product_name_inner_locator, timeout=3000, state="attached")
|
||||
product_price_element = await self.page.wait_for_selector(self.product_price_inner_locator, timeout=3000, state="attached")
|
||||
product_image_element = await self.page.wait_for_selector(self.product_image_inner_locator, timeout=3000, state="attached")
|
||||
|
||||
# 요소가 존재하면 정보 추출
|
||||
self.logger.debug(f"product_name_element : {product_name_element}")
|
||||
self.logger.debug(f"product_price_element : {product_price_element}")
|
||||
self.logger.debug(f"product_image_element : {product_image_element}")
|
||||
|
||||
if product_name_element and product_price_element and product_image_element:
|
||||
# await의 결과를 각 변수에 저장
|
||||
product_name_text = (await product_name_element.inner_text()).strip()
|
||||
product_price_text = (await product_price_element.inner_text()).strip()
|
||||
product_image_url = await product_image_element.get_attribute('src')
|
||||
|
||||
# product_info 딕셔너리에 결과 저장
|
||||
product_info = {
|
||||
"name": product_name_text,
|
||||
"price": product_price_text,
|
||||
"image_url": product_image_url
|
||||
}
|
||||
self.logger.debug(f"상품 {i}: {product_info}")
|
||||
product_infos.append(product_info)
|
||||
product_name_elements.append(product_name_element) # 각 product_name_element 추가
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"상품 {i} 정보 수집 중 오류 발생: {e}", exc_info=True)
|
||||
continue
|
||||
|
||||
return product_infos, product_name_elements # product_infos와 product_name_elements 함께 반환
|
||||
except Exception as e:
|
||||
self.logger.error(f"상품 정보 수집 중 오류 발생: {e}", exc_info=True)
|
||||
return []
|
||||
|
||||
|
||||
|
||||
|
||||
async def scroll_page_to_bottom(self, pause_time=0.2):
|
||||
"""페이지의 맨 아래까지 스크롤하여 모든 동적 요소를 로드"""
|
||||
self.logger.info('페이지 스크롤 시작...')
|
||||
previous_height = await self.page.evaluate("() => document.body.scrollHeight")
|
||||
|
||||
while True:
|
||||
await self.page.evaluate("window.scrollBy(0, window.innerHeight);") # 한 화면씩 스크롤
|
||||
await asyncio.sleep(pause_time) # 페이지 로딩 대기
|
||||
current_height = await self.page.evaluate("() => document.body.scrollHeight")
|
||||
if current_height == previous_height:
|
||||
break # 더 이상 스크롤할 내용이 없으면 종료
|
||||
previous_height = current_height
|
||||
self.logger.info('페이지 스크롤 완료.')
|
||||
|
||||
async def scroll_page_to_top(self, pause_time=0.2):
|
||||
"""페이지의 맨 위까지 스크롤"""
|
||||
self.logger.info('페이지 위로 스크롤 시작...')
|
||||
previous_height = await self.page.evaluate("() => window.pageYOffset")
|
||||
|
||||
while previous_height > 0:
|
||||
await self.page.evaluate("window.scrollBy(0, -window.innerHeight);") # 한 화면씩 위로 스크롤
|
||||
await asyncio.sleep(pause_time) # 페이지 로딩 대기
|
||||
current_height = await self.page.evaluate("() => window.pageYOffset")
|
||||
if current_height == previous_height:
|
||||
break # 더 이상 스크롤할 내용이 없으면 종료
|
||||
previous_height = current_height
|
||||
|
||||
self.logger.info('페이지 위로 스크롤 완료.')
|
||||
|
||||
|
||||
def run(self):
|
||||
asyncio.run(self.start_browser())
|
||||
|
||||
def terminate(self):
|
||||
self.logger.info("크롬 스레드 종료")
|
||||
self.cleanup() # 종료 시 추가 정리 작업 호출
|
||||
super().terminate()
|
||||
|
||||
def cleanup(self):
|
||||
if self.whale_browser:
|
||||
self.logger.info("Whale 브라우저 창 닫기 시도 중...")
|
||||
self.whale_browser.close_whale_window_if_exists()
|
||||
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
<assembly
|
||||
xmlns='urn:schemas-microsoft-com:asm.v1' manifestVersion='1.0'>
|
||||
<assemblyIdentity
|
||||
name='3.28.266.14'
|
||||
version='3.28.266.14'
|
||||
type='win32'/>
|
||||
<file name='whale_elf.dll'/>
|
||||
</assembly>
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
// This json file will contain a list of extensions that will be included
|
||||
// in the installer.
|
||||
|
||||
{
|
||||
}
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"name": "MEI Preload",
|
||||
"icons": {},
|
||||
"version": "1.0.7.1652906823",
|
||||
"manifest_version": 2,
|
||||
"update_url": "https://clients2.google.com/service/update2/crx",
|
||||
"description": "Contains preloaded data for Media Engagement"
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"manifest_version": 2,
|
||||
"name": "Privacy Sandbox Attestations",
|
||||
"version": "2024.7.12.0",
|
||||
"pre_installed": true
|
||||
}
|
||||
|
|
@ -0,0 +1,244 @@
|
|||
|
||||
https://2k.comhttps://33across.comhttps://360yield.comhttps://3lift.comhttps://ad-score.com
https://ad.gthttps://adentifi.comhttps://adform.nethttps://adingo.jphttps://admatrix.jphttps://admixer.nethttps://adnami.iohttps://adnxs.comhttps://adsafeprotected.comhttps://adsrvr.orghttps://adthrive.comhttps://advividnetwork.comNhttps://aggregation-service-site-dot-clz200258-datateam-italy.ew.r.appspot.comhttps://anonymised.iohttps://appier.nethttps://artistunited.comhttps://avads.nethttps://ayads.iohttps://bidtheatre.nethttps://bing.comhttps://blendee.comhttps://bounceexchange.comhttps://btloader.comhttps://bypass.jphttps://casalemedia.comhttps://cdn-net.comhttps://connected-stories.comhttps://crcldu.comhttps://creativecdn.comhttps://criteo.comhttps://ctnsnet.comhttps://dabbs.nethttps://daum.nethttps://display.iohttps://dotdashmeredith.comhttps://dotomi.comhttps://doubleclick.nethttps://dynalyst.jphttps://edkt.iohttps://effinity.frhttps://ezoic.comhttps://fanbyte.comhttps://flashtalking.comhttps://fout.jphttps://funplus.comhttps://gama.globohttps://ghtinc.comhttps://gmossp-sp.jphttps://google-analytics.comhttps://gsspat.jphttps://gumgum.comhttps://guoshipartners.comhttps://html-load.comhttps://im-apps.nethttps://impact-ad.jphttps://imrworldwide.comhttps://indexww.comhttps://inmobi.comhttps://innovid.comhttps://jivox.comhttps://kelkoogroup.nethttps://kidoz.nethttps://ladsp.comhttps://lucead.comhttps://mail.ruhttps://media.nethttps://mediaintelligence.dehttps://mediamath.comhttps://mediavine.comhttps://microad.jphttps://naver.comhttps://nhnace.comhttps://onetag-sys.comhttps://openx.nethttps://optable.cohttps://outbrain.com+https://privacy-sandbox-demos-ad-server.dev'https://privacy-sandbox-demos-dsp-a.dev'https://privacy-sandbox-demos-dsp-b.dev%https://privacy-sandbox-demos-dsp.dev'https://privacy-sandbox-demos-ssp-a.dev'https://privacy-sandbox-demos-ssp-b.dev%https://privacy-sandbox-demos-ssp.dev https://privacy-sandbox-test.com0https://privacy-sandcastle-dev-ad-server.web.app-https://privacy-sandcastle-dev-dsp-a1.web.app-https://privacy-sandcastle-dev-dsp-b1.web.app*https://privacy-sandcastle-dev-dsp.web.app,https://privacy-sandcastle-dev-ssp-a.web.app,https://privacy-sandcastle-dev-ssp-b.web.app*https://privacy-sandcastle-dev-ssp.web.apphttps://pub.networkhttps://pubmatic.comhttps://pubtm.comhttps://quantserve.comhttps://relevant-digital.comhttps://sascdn.comhttps://shinystat.comhttps://singular.nethttps://sportradarserving.comhttps://t13.iohttps://teads.tvhttps://theryn.iohttps://tncid.apphttps://toponad.comhttps://tpmark.nethttps://tribalfusion.comhttps://triptease.iohttps://uinterbox.comhttps://uol.com.br
https://vg.nohttps://vpadn.comhttps://washingtonpost.comhttps://yahoo.co.jphttps://yahoo.comhttps://yandex.ruhttps://yelp.com
|
||||
https://admission.net
|
||||
%
|
||||
https://audienceproject.com
|
||||
|
||||
https://thesun.co.uk
|
||||
|
||||
https://fandom.com
|
||||
|
||||
https://2trk.info
|
||||
|
||||
https://seedtag.com
|
||||
|
||||
https://adswizz.com
|
||||
|
||||
https://presage.io
|
||||
|
||||
https://aniview.com
|
||||
"
|
||||
https://audiencemanager.de
|
||||
|
||||
https://demand.supply
|
||||
|
||||
https://appscience.inc
|
||||
|
||||
https://semafor.com
|
||||
|
||||
https://linkedin.com
|
||||
|
||||
https://pinterest.com
|
||||
|
||||
https://atomex.net
|
||||
|
||||
https://superfine.org
|
||||
|
||||
https://postrelease.com
|
||||
|
||||
https://grxchange.gr
|
||||
|
||||
https://undertone.com
|
||||
|
||||
https://coupang.com
|
||||
|
||||
https://connatix.com
|
||||
"
|
||||
https://rubiconproject.com
|
||||
|
||||
https://globo.com
|
||||
|
||||
https://cazamba.com
|
||||
|
||||
https://tya-dev.com
|
||||
?
|
||||
6https://protected-audience-api-advertiser.onrender.com
|
||||
|
||||
https://retargetly.com
|
||||
|
||||
https://gokwik.co
|
||||
|
||||
https://deepintent.com
|
||||
|
||||
https://yieldlab.net
|
||||
|
||||
https://disqus.com
|
||||
|
||||
https://pontiac.media
|
||||
6
|
||||
/https://ptb-msmt-static-5jyy5ulagq-uc.a.run.app
|
||||
|
||||
https://metro.co.uk
|
||||
|
||||
https://apex-football.com
|
||||
|
||||
https://tiktok.com
|
||||
|
||||
https://eloan.co.jp
|
||||
"
|
||||
https://authorizedvault.com
|
||||
%
|
||||
https://wepowerconnections.com
|
||||
|
||||
https://trip.com
|
||||
|
||||
https://lwadm.com
|
||||
|
||||
https://s-f.tech
|
||||
"
|
||||
https://explorefledge.com
|
||||
|
||||
https://momento.dev
|
||||
|
||||
https://sitescout.com
|
||||
|
||||
https://samplicio.us
|
||||
|
||||
https://pmdragonfly.com
|
||||
|
||||
https://logly.co.jp
|
||||
|
||||
https://dailymail.co.uk
|
||||
"
|
||||
https://kompaspublishing.nl
|
||||
|
||||
https://trkkn.com
|
||||
|
||||
https://gunosy.com
|
||||
|
||||
https://socdm.com
|
||||
|
||||
https://boost-web.com
|
||||
|
||||
https://moshimo.com
|
||||
|
||||
https://appconsent.io
|
||||
"
|
||||
https://media6degrees.com
|
||||
(
|
||||
https://smadexprivacysandbox.com
|
||||
|
||||
https://weborama.fr
|
||||
"
|
||||
https://d-edgeconnect.media
|
||||
|
||||
https://torneos.gg
|
||||
|
||||
https://mobon.net
|
||||
|
||||
https://yieldmo.com
|
||||
|
||||
https://iobeya.com
|
||||
|
||||
https://dreammail.jp
|
||||
#
|
||||
https://marutishanbhag.com
|
||||
|
||||
https://bluems.com
|
||||
|
||||
https://taboola.com
|
||||
|
||||
https://usemax.de
|
||||
|
||||
https://atirun.com
|
||||
!
|
||||
https://audience360.com.au
|
||||
|
||||
https://beaconmax.com
|
||||
|
||||
https://primecaster.net
|
||||
|
||||
https://acxiom.com
|
||||
|
||||
https://ad-stir.com
|
||||
|
||||
https://facebook.com
|
||||
!
|
||||
https://sharethrough.com
|
||||
|
||||
https://wp.pl
|
||||
|
||||
https://adroll.com
|
||||
|
||||
https://permutive.app
|
||||
|
||||
https://vidazoo.com
|
||||
&
|
||||
https://googleadservices.com
|
||||
|
||||
https://nexxen.tech
|
||||
|
||||
https://quora.com
|
||||
|
||||
https://appsflyer.com
|
||||
"
|
||||
https://amazon-adsystem.com
|
||||
|
||||
https://a-mo.net
|
||||
|
||||
https://verve.com
|
||||
|
||||
https://onet.pl
|
||||
|
||||
https://worldhistory.org
|
||||
|
||||
https://convertunits.com
|
||||
|
||||
https://unrulymedia.com
|
||||
|
||||
https://getyourguide.com
|
||||
!
|
||||
https://dailymotion.com
|
||||
|
||||
https://tailtarget.com
|
||||
|
||||
https://finn.no
|
||||
$
|
||||
https://lab-dotmetrics.ninja
|
||||
|
||||
https://stackadapt.com
|
||||
|
||||
https://i-mobile.co.jp
|
||||
#
|
||||
https://adsmeasurement.com
|
||||
%
|
||||
https://googlesyndication.com
|
||||
|
||||
https://open-bid.com
|
||||
|
||||
https://sephora.com
|
||||
|
||||
https://tangooserver.com
|
||||
|
||||
https://docomo.ne.jp
|
||||
|
||||
https://jkforum.net
|
||||
%
|
||||
https://creative-serving.com
|
||||
!
|
||||
https://ebayadservices.com
|
||||
|
||||
https://r2b2.io
|
||||
#
|
||||
https://youronlinechoices.eu
|
||||
|
||||
https://snapchat.com
|
||||
|
||||
https://adscale.de
|
||||
|
||||
https://aqfer.com
|
||||
|
||||
https://kargo.com
|
||||
|
||||
https://storygize.net
|
||||
|
||||
https://ebis.ne.jp
|
||||
|
||||
https://shinobi.jp
|
||||
!
|
||||
https://weborama-tech.ru
|
||||
|
||||
https://cpx.to
|
||||
1
|
||||
(https://paa-reporting-advertising.amazon
|
||||
|
||||
|
After Width: | Height: | Size: 30 KiB |
|
After Width: | Height: | Size: 55 KiB |
|
After Width: | Height: | Size: 55 KiB |
|
After Width: | Height: | Size: 52 KiB |
|
After Width: | Height: | Size: 9.9 KiB |
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
|
@ -0,0 +1,14 @@
|
|||
{
|
||||
"APP_NAME": {
|
||||
"message": "Smart Lens"
|
||||
},
|
||||
"APP_DESCRIPTION": {
|
||||
"message": "Smart Lens Extension"
|
||||
},
|
||||
"CONTEXT_MENU_TITLE": {
|
||||
"message": "Search with image"
|
||||
},
|
||||
"IMAGE_SEARCH_FAIL": {
|
||||
"message": "Image search failed. Please try again soon."
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
{
|
||||
"APP_NAME": {
|
||||
"message": "스마트렌즈"
|
||||
},
|
||||
"APP_DESCRIPTION": {
|
||||
"message": "스마트렌즈 확장앱"
|
||||
},
|
||||
"CONTEXT_MENU_TITLE": {
|
||||
"message": "이미지로 검색"
|
||||
},
|
||||
"IMAGE_SEARCH_FAIL": {
|
||||
"message": "이미지 검색에 실패했습니다. 잠시 후 다시 시도해주세요."
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,104 @@
|
|||
{
|
||||
"APP_NAME": { "message": "QuickSearch" },
|
||||
"LANG_AM": { "message": "Amharic" },
|
||||
"LANG_AR": { "message": "Arabic" },
|
||||
"LANG_AS": { "message": "Assamese" },
|
||||
"LANG_AZ": { "message": "Azerbaijani" },
|
||||
"LANG_BE": { "message": "Belarusian" },
|
||||
"LANG_BG": { "message": "Bulgarian" },
|
||||
"LANG_BN": { "message": "Bengali" },
|
||||
"LANG_BR": { "message": "Portuguese" },
|
||||
"LANG_BS": { "message": "Bosnian" },
|
||||
"LANG_CA": { "message": "Catalan" },
|
||||
"LANG_CS": { "message": "Czech" },
|
||||
"LANG_CY": { "message": "Welsh" },
|
||||
"LANG_DA": { "message": "Danish" },
|
||||
"LANG_DE": { "message": "German" },
|
||||
"LANG_EL": { "message": "Greek" },
|
||||
"LANG_EN": { "message": "English" },
|
||||
"LANG_ES": { "message": "Spanish" },
|
||||
"LANG_ET": { "message": "Estonian" },
|
||||
"LANG_EU": { "message": "Basque" },
|
||||
"LANG_FA": { "message": "Persian" },
|
||||
"LANG_FI": { "message": "Finnish" },
|
||||
"LANG_FR": { "message": "French" },
|
||||
"LANG_GA": { "message": "Irish" },
|
||||
"LANG_GD": { "message": "Scottish Gaelic" },
|
||||
"LANG_GL": { "message": "Galician" },
|
||||
"LANG_GU": { "message": "Gujarati" },
|
||||
"LANG_HI": { "message": "Hindi" },
|
||||
"LANG_HR": { "message": "Croatian" },
|
||||
"LANG_HU": { "message": "Hungarian" },
|
||||
"LANG_HY": { "message": "Armenian" },
|
||||
"LANG_ID": { "message": "Indonesian" },
|
||||
"LANG_IG": { "message": "Igbo" },
|
||||
"LANG_IS": { "message": "Icelandic" },
|
||||
"LANG_IT": { "message": "Italian" },
|
||||
"LANG_IW": { "message": "Hebrew" },
|
||||
"LANG_JA": { "message": "Japanese" },
|
||||
"LANG_KA": { "message": "Georgian" },
|
||||
"LANG_KK": { "message": "Kazakh" },
|
||||
"LANG_KM": { "message": "Khmer" },
|
||||
"LANG_KN": { "message": "Kannada" },
|
||||
"LANG_KO": { "message": "Korean" },
|
||||
"LANG_KY": { "message": "Kyrgyz" },
|
||||
"LANG_LB": { "message": "Luxembourgish" },
|
||||
"LANG_LO": { "message": "Lao" },
|
||||
"LANG_LT": { "message": "Lithuanian" },
|
||||
"LANG_LV": { "message": "Latvian" },
|
||||
"LANG_MK": { "message": "Macedonian" },
|
||||
"LANG_ML": { "message": "Malayalam" },
|
||||
"LANG_MN": { "message": "Mongolian" },
|
||||
"LANG_MR": { "message": "Marathi" },
|
||||
"LANG_MS": { "message": "Malay" },
|
||||
"LANG_MT": { "message": "Maltese" },
|
||||
"LANG_NE": { "message": "Nepali" },
|
||||
"LANG_NL": { "message": "Dutch" },
|
||||
"LANG_NN": { "message": "Norwegian Nynorsk" },
|
||||
"LANG_NO": { "message": "Norwegian Bokmål" },
|
||||
"LANG_OR": { "message": "Oriya" },
|
||||
"LANG_PA": { "message": "Punjabi" },
|
||||
"LANG_PL": { "message": "Polish" },
|
||||
"LANG_PT": { "message": "Portuguese" },
|
||||
"LANG_QU": { "message": "Quechua" },
|
||||
"LANG_RO": { "message": "Romanian" },
|
||||
"LANG_RU": { "message": "Russian" },
|
||||
"LANG_RW": { "message": "Kinyarwanda" },
|
||||
"LANG_SI": { "message": "Sinhala" },
|
||||
"LANG_SK": { "message": "Slovak" },
|
||||
"LANG_SL": { "message": "Slovenian" },
|
||||
"LANG_SQ": { "message": "Albanian" },
|
||||
"LANG_SR": { "message": "Serbian" },
|
||||
"LANG_SV": { "message": "Swedish" },
|
||||
"LANG_SW": { "message": "Swahili" },
|
||||
"LANG_TA": { "message": "Tamil" },
|
||||
"LANG_TE": { "message": "Telugu" },
|
||||
"LANG_TH": { "message": "Thai" },
|
||||
"LANG_TI": { "message": "Tigrinya" },
|
||||
"LANG_TL": { "message": "Filipino" },
|
||||
"LANG_TR": { "message": "Turkish" },
|
||||
"LANG_UG": { "message": "Uyghur" },
|
||||
"LANG_UK": { "message": "Ukrainian" },
|
||||
"LANG_UR": { "message": "Urdu" },
|
||||
"LANG_UZ": { "message": "Uzbek" },
|
||||
"LANG_VI": { "message": "Vietnamese" },
|
||||
"LANG_YO": { "message": "Yoruba" },
|
||||
"LANG_ZHCN": { "message": "Chinese Simplified" },
|
||||
"LANG_ZHTW": { "message": "Chinese Traditional" },
|
||||
"LANG_ZU": { "message": "Zulu" },
|
||||
"MENU_SEARCH": { "message": "Search" },
|
||||
"MENU_SEARCH_CTX": { "message": "QuickSearch for selected text" },
|
||||
"MENU_TIMELINE": { "message": "Add to Timeline" },
|
||||
"MENU_TRANSL": { "message": "Translate" },
|
||||
"SHOW_MORE": { "message": "Show More" },
|
||||
"TRANS_ERROR": { "message": "Error occurred" },
|
||||
"TRANS_IS_NOT_SUPPORTED_LANGUAGE": { "message": "$1 is not supported" },
|
||||
"TRANS_NOT_SUPPORTED_LANGUAGE": { "message": "Not supported Language" },
|
||||
"TRANS_PROGRESS": { "message": "Translating..." },
|
||||
"TRANS_REPLACE": { "message": "Replace with translated text" },
|
||||
"TRANS_SRCLANG": { "message": "$1" },
|
||||
"TRANS_TARLANG": { "message": "$1" },
|
||||
"CURRENCY_DATE": { "message": "Last update on"},
|
||||
"MAP_LARGER": { "message": "View Larger Map" },
|
||||
"QUICKSEARCH": { "message": "QuickSearch" }
|
||||
}
|
||||
|
|
@ -0,0 +1,104 @@
|
|||
{
|
||||
"APP_NAME": { "message": "퀵서치" },
|
||||
"LANG_AM": { "message": "암하라어" },
|
||||
"LANG_AR": { "message": "아랍어" },
|
||||
"LANG_AS": { "message": "아삼어" },
|
||||
"LANG_AZ": { "message": "아제르바이잔어" },
|
||||
"LANG_BE": { "message": "벨라루스어" },
|
||||
"LANG_BG": { "message": "불가리아어" },
|
||||
"LANG_BN": { "message": "벵골어" },
|
||||
"LANG_BR": { "message": "브라질 포르투갈어" },
|
||||
"LANG_BS": { "message": "보스니아어" },
|
||||
"LANG_CA": { "message": "카탈로니아어" },
|
||||
"LANG_CS": { "message": "체코어" },
|
||||
"LANG_CY": { "message": "웨일스어" },
|
||||
"LANG_DA": { "message": "덴마크어" },
|
||||
"LANG_DE": { "message": "독일어" },
|
||||
"LANG_EL": { "message": "그리스어" },
|
||||
"LANG_EN": { "message": "영어" },
|
||||
"LANG_ES": { "message": "스페인어" },
|
||||
"LANG_ET": { "message": "에스토니아어" },
|
||||
"LANG_EU": { "message": "바스크어" },
|
||||
"LANG_FA": { "message": "페르시아어" },
|
||||
"LANG_FI": { "message": "핀란드어" },
|
||||
"LANG_FR": { "message": "프랑스어" },
|
||||
"LANG_GA": { "message": "아일랜드어" },
|
||||
"LANG_GD": { "message": "스코틀랜드 게일어" },
|
||||
"LANG_GL": { "message": "갈리시아어" },
|
||||
"LANG_GU": { "message": "구자라트어" },
|
||||
"LANG_HI": { "message": "힌디어" },
|
||||
"LANG_HR": { "message": "크로아티아어" },
|
||||
"LANG_HU": { "message": "헝가리어" },
|
||||
"LANG_HY": { "message": "아르메니아어" },
|
||||
"LANG_ID": { "message": "인도네시아어" },
|
||||
"LANG_IG": { "message": "이그보어" },
|
||||
"LANG_IS": { "message": "아이슬란드어" },
|
||||
"LANG_IT": { "message": "이탈리아어" },
|
||||
"LANG_IW": { "message": "히브리어" },
|
||||
"LANG_JA": { "message": "일본어" },
|
||||
"LANG_KA": { "message": "조지아어" },
|
||||
"LANG_KK": { "message": "카자흐어" },
|
||||
"LANG_KM": { "message": "캄보디아어" },
|
||||
"LANG_KN": { "message": "칸나다어" },
|
||||
"LANG_KO": { "message": "한국어" },
|
||||
"LANG_KY": { "message": "키르기스어" },
|
||||
"LANG_LB": { "message": "룩셈부르크어" },
|
||||
"LANG_LO": { "message": "라오어" },
|
||||
"LANG_LT": { "message": "리투아니아어" },
|
||||
"LANG_LV": { "message": "라트비아어" },
|
||||
"LANG_MK": { "message": "마케도니아어" },
|
||||
"LANG_ML": { "message": "말라얄람어" },
|
||||
"LANG_MN": { "message": "몽고어" },
|
||||
"LANG_MR": { "message": "마라티어" },
|
||||
"LANG_MS": { "message": "말레이어" },
|
||||
"LANG_MT": { "message": "몰타어" },
|
||||
"LANG_NE": { "message": "네팔어" },
|
||||
"LANG_NL": { "message": "네덜란드어" },
|
||||
"LANG_NN": { "message": "노르웨이어" },
|
||||
"LANG_NO": { "message": "노르웨이어" },
|
||||
"LANG_OR": { "message": "오리야어" },
|
||||
"LANG_PA": { "message": "펀잡어" },
|
||||
"LANG_PL": { "message": "폴란드어" },
|
||||
"LANG_PT": { "message": "포르투갈어" },
|
||||
"LANG_QU": { "message": "케추아어" },
|
||||
"LANG_RO": { "message": "루마니아어" },
|
||||
"LANG_RU": { "message": "러시아어" },
|
||||
"LANG_RW": { "message": "르완다어" },
|
||||
"LANG_SI": { "message": "스리랑카어" },
|
||||
"LANG_SK": { "message": "슬로바키아어" },
|
||||
"LANG_SL": { "message": "슬로베니아어" },
|
||||
"LANG_SQ": { "message": "알바니아어" },
|
||||
"LANG_SR": { "message": "세르비아어" },
|
||||
"LANG_SV": { "message": "스웨덴어" },
|
||||
"LANG_SW": { "message": "스와힐리어" },
|
||||
"LANG_TA": { "message": "타밀어" },
|
||||
"LANG_TE": { "message": "텔루구어" },
|
||||
"LANG_TH": { "message": "태국어" },
|
||||
"LANG_TI": { "message": "티그리냐어" },
|
||||
"LANG_TL": { "message": "필리핀어" },
|
||||
"LANG_TR": { "message": "튀르키예어" },
|
||||
"LANG_UG": { "message": "위구르어" },
|
||||
"LANG_UK": { "message": "우크라이나어" },
|
||||
"LANG_UR": { "message": "우르두어" },
|
||||
"LANG_UZ": { "message": "우즈베크어" },
|
||||
"LANG_VI": { "message": "베트남어" },
|
||||
"LANG_YO": { "message": "요루바어" },
|
||||
"LANG_ZHCN": { "message": "중국어 간체" },
|
||||
"LANG_ZHTW": { "message": "중국어 번체" },
|
||||
"LANG_ZU": { "message": "줄루어" },
|
||||
"MENU_SEARCH": { "message": "검색" },
|
||||
"MENU_SEARCH_CTX": { "message": "선택한 텍스트 퀵서치" },
|
||||
"MENU_TIMELINE": { "message": "타임라인에 담기" },
|
||||
"MENU_TRANSL": { "message": "번역" },
|
||||
"SHOW_MORE": { "message": "더보기" },
|
||||
"TRANS_ERROR": { "message": "오류가 발생하여 번역할 수 없습니다" },
|
||||
"TRANS_IS_NOT_SUPPORTED_LANGUAGE": { "message": "$1는 번역할 수 없습니다" },
|
||||
"TRANS_NOT_SUPPORTED_LANGUAGE": { "message": "번역할 수 없는 언어입니다" },
|
||||
"TRANS_PROGRESS": { "message": "번역 중..." },
|
||||
"TRANS_REPLACE": { "message": "선택영역에 번역결과 붙여넣기" },
|
||||
"TRANS_SRCLANG": { "message": "$1" },
|
||||
"TRANS_TARLANG": { "message": "$1" },
|
||||
"CURRENCY_DATE": { "message": "기준일시" },
|
||||
"MAP_LARGER": { "message": "지도보기" },
|
||||
"QUICKSEARCH": { "message": "퀵서치" }
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
{"WIDGET_TITLE":{"message":"Toolbox"}}
|
||||
|
|
@ -0,0 +1 @@
|
|||
{"WIDGET_TITLE":{"message":"도구모음"}}
|
||||
|
|
@ -0,0 +1,108 @@
|
|||
############################################################
|
||||
# SenseTime License
|
||||
# License Product: SenseME
|
||||
# Expiration: 20240418~20250401
|
||||
# License SN: fc28ddcc-1e8d-4785-bfa7-8a67a10107a7
|
||||
############################################################
|
||||
sGfddw4LIYfDH8JVexvCpWpU2GL5PTf3iG+61UjmSlry6hawzCZn/oLHD2U/
|
||||
I20rL0g71a08OisB9lUrqQjP4W7jWqMIjhQXiUEl7fjfDCBrNs3FT3SBxEWO
|
||||
5gWqqdv5sCCotC496NbPVRoIdZHironkCSliF4nlVLGK+tppJDDARvpTAQAA
|
||||
AAIAAAB3MR/56os6Wol3CGWOzPtl2UosZ1rxj0TRnCd0K2MwgcZG7aWds6dg
|
||||
I39zWeewITeLAHAS7eJw6FSfDIeUPQjTyaupBaD2SuojzrncxvscEPNlJiV7
|
||||
2WYCOlPRVVX/hJmd2UZKNxZz6ByqMCPy88yMwRlw2GtshwqL0EXnHsNdQaR1
|
||||
SDrsER0edwkvAR69qg/MYwP3qtl7sTp92MkTS2HbE4rJItJUedwfjuttB0kG
|
||||
uxZ6x613Ws5TUeH0sYuNJcVgsM6C+mlQEDSqjNIEqphmdvJPswLC2v5WxVsV
|
||||
Vz8GLLdyzhE6T0hAWDPQCFxo587uELYq4+OZwRU+YmpADLwHAQABAAAAAAAD
|
||||
AAAAAAAAAAAAAAARrz2I+9OFMLMa3o+8Ynq6UTw1HBB6fp6AJ8ekSLfdlGAg
|
||||
OPPZOt0tsJmINmliIjk/jfDLhM0xS3LfpKoT0kvxS5sywd2KsA/Z4zoWYToY
|
||||
UIZL9IJqQuNNZRZ1kZy/cOPi5EqkwsXJPArbJM0s/MxvnxzhQBTdGL3TsPhN
|
||||
XeS12nKtQBXHBO6BwgvrhGxpDMtR4l/JyC0j4NFehW6HDU6j2Jlm4xfBVerO
|
||||
79DcSGllQNxGmH3tta9cA7T34pAMln7MgU7WTIPUuKq18QjATXYI3JjNV4gZ
|
||||
f6A+Gv6muOnwXQD+ag/E0epKyjm3wFnBxwIwn0uJD6f2O1VF4p66Amg8UEK6
|
||||
X8FL9nPENF55P/uUz1COvO2NkBtgA5zIK+pgOXIHCCUOS2zRDYKfizm7mc4f
|
||||
V6JcE0sx+VdzFps8Y80PTth+hHxT3QgZ3mVMnEl5TMs9jhw7xvY14yj+kw7G
|
||||
4/fzhxxxxlE7zYp9rv7yHct6pZ0c8U9ET3JD6qADmYa+8aMEo5ZhG8Hw7hQl
|
||||
sbhE16qNJZzdvOGdig5O8HuZnWC5UhoWOwK+UE5AgJoGNNxvFja59H84D8fM
|
||||
cMnMdQQPxY0MuCLjCY3Mm5oYb2F83MtnB0KOToPh3KAUvwleYtj1Dy+SFitu
|
||||
nZ1yn0CsE1by4KXeaOsIyA1K+p8+4gluqrT8px5ovi9ZqSW1HgYbk4VQTc7P
|
||||
7wMJuvv4bbQJvq7FJ7t47mgcDwW1HNkW8cxbh1NRMDa8/5wZ/2vXJ1HLk1Yn
|
||||
JCiHCM75U6IajGmYDPna/maaf/nXYHIW72yOrU/9mhBg7gQwVJs9E/uJiJec
|
||||
80WiS9MLT45mcXTWbOpUH5ofcPU8K6bMXTeTpHKeQ/+bNWhrXt20MuD6Oq3v
|
||||
iPBW/rkqGSV8NRTRHAfVFFUlgNwuflYiWEux7tiMnDkzCOyk7CMGNHuf0H6d
|
||||
IvpHSb6CZRUSQwLqO5YDGsX08B2hIexorMDf39vEWrJe9cBYndW3sa4GCUgW
|
||||
XSjZFFh7GOGU6qJo2BEtMaCjU1KDDsB4FwTHR+iLxUuRsa9yW2PU70fwbAlB
|
||||
zp5uY7j/hKk5weOQlsROn3oJKHho6cHpjPxPsruaAbDq+bPgGfDb/POotpgI
|
||||
8/UnYrPaKJOZRvpA1rx60ew69TqzE/9ltRncWST7BEzdjR7kre/EEgiR3es7
|
||||
G/9KrhX8Vq14N6uYNlpHR3te5EvMErGedaOWvcnnL25N1qyiUfVzAcGnZPA4
|
||||
Wf+dLO7oBVkoJqKF11PQiYJn7C9IvosztHRgLo5mYXQid7Afl96I8itWsrJD
|
||||
P3A8yjtiPAHM28amRBBQU1DDQ2Ea18CQwnnUm3qX98/c5xzxPw44XEh+OwZJ
|
||||
xOutU+9avHn29ND3iQrUQEMw6BCKrL6ST4/H/mdyAMB/Ki4XrKiolYP4S2NG
|
||||
Eg2V+l2T6OftoH6sRcwphDdvOOKvlLqn5X6N8FEG6Ccl4AHkuc56iuLB1E1h
|
||||
B/nmVESlZ90sEAni/c4aTxkdg35pyS32Oye58acpX/TsM2RsGsup1BUmmWp/
|
||||
InAfuTirAIZRWwlk8OGQI3l8rdn4UNa3KaMS24YHz+3WoQR0Io9CvClmx2N6
|
||||
y/CMaF47HbVX/zj9qJevObhq5OgtiXzs27q+0dsgRPPNXoyXwCVczZH+Qdds
|
||||
h+L/K6oujcZaNlmy+wKi89dEqHBIcv/FScu5kFNlllukLJU4cmh9omjcotBu
|
||||
R4bxUyXKEba6BsYWkJciE0LUaTcvxplWxlozuScIsfBtO2aaUzEQf2ALUKRo
|
||||
2c2srEU/L4BTB73np7PtZwanURoZAiNae1noEETMmly60VBM9X8XAHPjP/Cg
|
||||
2djlAFJKxnDBSNsKn6RXo3zGi8ZZogKgES9yXYm43JaNJEqWeoLNI/wCJbuJ
|
||||
tpmgLt7OHPuGd0yCP9+1oj8xA44kbvAeyFedxw+FpEM1zrZ2AgbkRh+lA6Nl
|
||||
wSPelo7woVdsUooXJsrvBlXr2aEPY6uX9ZRZKOnksxci1oD7e9UEQvneBhGA
|
||||
8smJEukpM/qTgqkmF93gZKKegFlWH7QJZZB415xmPuuR5uZ1FlvyuWx7INiN
|
||||
i+ihaIOcT5lTcYGSujl69+Wto5OPkrGlUObCjBlxfkdnkSbmUJ1P2jwPE+X5
|
||||
ucNuqso8zi1GnAblTlmbf+pKw2YglWiFox9gSib6Z3vOYT/cECuy8v1j781z
|
||||
1BaPrn2+VHX1CDyBH0Ax7W/darF7ACEgwplbJ4uriag9tiS88PMR9uHSZdP7
|
||||
IuJ1ln2/FhrRVSKllda6fFEu7xz1f9MqFo6pip3Hotade4JNpWVF4bb+D5GN
|
||||
b1r+w7wSmENsQ8OSIcN0zXtmzijnvTGnL4M/AO732mPF/1M/KHBaILteM3P+
|
||||
JJp1vkrDz3fUZ2NvvXB/p1H6jJQhP37xqymrhzyjz+WCUq7rcEY2MfdnI94u
|
||||
h+dAfUAvEqS9/HSe5uwAV6HDVfc6szRj5FznMpy/DI71a4ZT99RYpZYtFBuw
|
||||
Pzz/6TPZ+Iqfc6f8XH/aoEzRIZQHPAl94/5JuwPvmFTKAg+nec8+/oDxpE36
|
||||
Cu4S6/Etkh/JVXhLSbCoQRHlvKkuPO3OeefeTMAUdiXdGvwltXzcas6FPzdZ
|
||||
nfWFJKHugtAtnzA2NUiE3Z8bLedr+jv29so5lYhkz9uUVx/nn2/16xXTwhQZ
|
||||
KjPnPaXqc6ucYyr2Td8l59iylSEhECRXs0sfTDfyIBYTWP+3EL3WLVsrHwNE
|
||||
xmMtAhc6u8WdWFZdF9Lanmu//8Yko7ARxhg7V9epV+6uKvOTQDokLRT1A/x6
|
||||
ArrF/HUSog0QYvnY4+Hg/4zY8TmVTOWspzcJG/0Hx+RMF+4gW/tbbJC6MGUg
|
||||
oXLZUpnx68ANKND6Ou4bbei+W7Mn6ToOppIDr53MZwbfQD36ci0HSWiov5ur
|
||||
eJspp7dZXJl/XGGmncr0dRrUdGxa5qPKFnHGjb15H1MxNyVax4pLNejxkm/W
|
||||
/WIRsXt41lLoasApVhRelmLM2lv13NKiT19L/xu9IHh1+gVtqnxTvf8HW8Yw
|
||||
oh6GXhQ5furjyRWMHD1EEiwgx0lILV4s2xDDe3Ugkndd0AFnmU5cPLc2nF8P
|
||||
2Ryj1p/vvnb09qGNoaPcGPmbrfXRY5HswC189lm4Dq6o0ukEfRbamJEg947v
|
||||
JKWn44by2f7h5za3AS77Y5//FxtL82H5MujbTBzVKqRTpu7Hbuw7NDmMeqRm
|
||||
lQfT3LABhzzAtwqbEofIk4XDnT7hNXs8y/6RB1lsW1hcIep0qeCz5IRPUNrr
|
||||
r7hN9uAccshyQ8atBHi3MIivaAKP26sBGsLF2ypqt2OTSoZuprPUYOjXik+W
|
||||
F/hkEAhT48H6z7OtIaPku0R6QanSTwW+WZgGHeqVVeLP5m1HfID+J5Mc3vK3
|
||||
EZlHDvO4AiSdXVerHkoICGnT9p7g9pKsgncYNrt2+JjJJibCcT3fA2wINX2r
|
||||
fZY6ntl4kFWGaEM1bIvRJxoPm5phSDxXYXtrImaD/OZr/1CMf1pVPJskNinK
|
||||
MzeohRAuDga8J/x4JBXUsVRyM03lSiZyJIlrPeIM71TN6a4qQplBMLPbyX4P
|
||||
/YMHcqANRnpzDvZQvZMHvmkaQUuYDBpY8MK2gOAEHYsVbMdQTSuvLR90yoqF
|
||||
Qr4LFyZVPRzYIaUEVYkczUvYooSL0iBLLkaTLRAWpm/XfAu+7rzp71hkeaKT
|
||||
tVo1BWa+Y08PxS2wXu2gc7jd3z9babp2+mI+e0f0gc5yCcbDAJk38ls8rQQr
|
||||
bFZWdg0JyeJ4Ohx66SAMom2QNZAWh4Ue8nWw5OGdBWpc6eUONe8iPpXOkAUo
|
||||
wQNT6CieIlQP3CJzBySW23jfUHo5XuzN9J1TxGI16QoLi66cOULCjKV0xM0W
|
||||
m8kYaLCQ138rtI35vLVwBi5MKh9acO1v3kStNUqIF/xH52wilvXBGT+4Hhty
|
||||
Xt0IdSEwRX8xX+ZNJpmrSw4lCxjWPHJbQu4mZ8/WOAFYGQOQ7JgI97R8Rr6F
|
||||
/buTwgsQ38s2Wc9NupGPMz9KMMJz4zuCVp+NNBvrqfBjDdyFGT48vvDB1yFs
|
||||
gPMj/VDPcKPGqbzzcJ5uoKxLSlIn+WF2kubjM5joMe25Z1czIVvHjftVDUcO
|
||||
phCPx2Xue3+2ZSVnSW/GUsqCYQBQ4gm68OKu3ihyyu8gSm/2ST8809KREEMJ
|
||||
/ZR3FYTmyl3Caw/NzZxL0HSOMXOOhPYQJoH/wCJYdpIbf5BEgFyVjDq/YI6g
|
||||
6SX9yFmeyPEAffKkCx/u6/Gqksaj34pPkjfuv2n8ET2vJbxHpUIunKWFZnqf
|
||||
5HmawEMSbtlbWCnA09TR9CmOYdPPNEySyBCHYjM/xOU9BBl/qxgXhIAFX7W0
|
||||
VWGX7r5rj3lTuAGEWUKKTO+VJHa0EDwlgGhKCXNodWOzTLLfBAIaL/uxspwS
|
||||
fcrVzmePpjC0f+alVc156q+MfOyD01hl/+68hzqLqi8EcLqhKYPpPUanWmzo
|
||||
0F7M0EUjwr020lMlEbKZHHJ75CZugpJ8TDu13E+IofK+KwfijgESMtSRlIzm
|
||||
aw6wD7rlJ/9+5YhiiMQ0SUPDlsxqVEUcsKTviYiBag+c+K9PVlkhgq1kIZqq
|
||||
gftnhn0ADGdBv+CWyGVyfp0wwPHv7Bjs8idDQ+xhxBkWGO1AU4SvOXSXFE26
|
||||
IdOeQY4TSP+8E5R0AzpH9nRUNmJBUZglntvElbpEL0D02lMWsAL6rzPhbl49
|
||||
Wri+i8BAETy3k2ong39CbecSX+tR1UsTw4NJfCZ6l+IyhQjI1JSpylraBNKK
|
||||
gz7M43I9JWvns1JXwA2DAPN4IDh5lyahZpTBmEwUkgV5UaYZ6QJ2xC38ZfJs
|
||||
kYpGRlvDpQTpZEnfjJJj2OlfmWaO8s74SU1l4kFrTuoQTqCdhEgfYWhvTyoq
|
||||
k5Ri7UZdmSfsrcdChu/NbeSe7fpfcbrlfPmMRw9q0IzFyVubLJ4vyleDPQX2
|
||||
dMDuSfYiy0+N6r2x/c1tJLbw+Ja5rbEgLAH3H7CY1/gx4cTA0nOw2HYOKudp
|
||||
TujzPPyN8GvAF8isKC9YQGd2Su+zKeVpXM1VfYYCjWjYySyqHA3Nss15BGr7
|
||||
eIutSYqBj5JSEx8xfShd4fVAVdZf5zZmjk3T7754+W6xjtQhQFg3f8ibNFos
|
||||
cT3MAmST1VxGNni2VLKNILGXU8IwfyKxgiK/qnoc+/0E7kbeWdS3/aV9CADY
|
||||
OvalR10MgVEd4tvPUx5CujdeFxXYQvIklezRO5IsChVBO68SNJivE5GK4337
|
||||
uKiX+7R8o1SZGOIzBuqaJSMr7xHP6m0BO1c9xEKy+tDqPfdoHLV8kALXQ6y4
|
||||
0lVOw24fk/Jwc8qGjG6k0TwcvmHWicBp7cjEv9S5r3GAtxkXq2ItL0faL1gQ
|
||||
WInfEvknBOsne4mXzBE/lDRL1Czwe5oo2wwFC5wtUpSpsAfBfxtPFPIHBAcj
|
||||
WwlE7jgXbgYA3PdAbO4qlvcoQ/lwV9SV/sD3Z90so9SvhH37JW0ykExmFKsQ
|
||||
W+zL+Kvilw0KxOcUHLvwBzV/gy4j0U71XiLrJDE/etpyuSrQSZBkAka1evc=
|
||||