스레드 통합중

This commit is contained in:
9700X_PC 2024-11-08 16:22:34 +09:00
parent c97845e021
commit 93c9609d40
130 changed files with 6325 additions and 1562 deletions

File diff suppressed because it is too large Load Diff

976
browser_control2.py Normal file
View File

@ -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>&nbsp;</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>&nbsp;</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()

View File

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

Binary file not shown.

View File

@ -0,0 +1,5 @@
// This json file will contain a list of extensions that will be included
// in the installer.
{
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

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

Binary file not shown.

View File

@ -0,0 +1,6 @@
{
"manifest_version": 2,
"name": "Privacy Sandbox Attestations",
"version": "2024.7.12.0",
"pre_installed": true
}

View File

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


Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

File diff suppressed because it is too large Load Diff

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

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

View File

@ -0,0 +1,14 @@
{
"APP_NAME": {
"message": "스마트렌즈"
},
"APP_DESCRIPTION": {
"message": "스마트렌즈 확장앱"
},
"CONTEXT_MENU_TITLE": {
"message": "이미지로 검색"
},
"IMAGE_SEARCH_FAIL": {
"message": "이미지 검색에 실패했습니다. 잠시 후 다시 시도해주세요."
}
}

View File

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

View File

@ -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": "퀵서치" }
}

View File

@ -0,0 +1 @@
{"WIDGET_TITLE":{"message":"Toolbox"}}

View File

@ -0,0 +1 @@
{"WIDGET_TITLE":{"message":"도구모음"}}

View File

@ -0,0 +1,108 @@
############################################################
# SenseTime License
# License Product: SenseME
# Expiration: 20240418~20250401
# License SN: fc28ddcc-1e8d-4785-bfa7-8a67a10107a7
############################################################
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

Some files were not shown because too many files have changed in this diff Show More