가격탭 추가

config.ini로 선택자 관리
This commit is contained in:
Envy_PC 2024-10-06 17:40:49 +09:00
parent 591c22fef4
commit 3dff828d71
10 changed files with 1887 additions and 148 deletions

View File

@ -8,9 +8,10 @@ from bs4 import BeautifulSoup
import asyncio import asyncio
class BrowserController: class BrowserController:
def __init__(self, app, logger): def __init__(self, app, logger, locator_manager):
self.app = app self.app = app
self.logger = logger self.logger = logger
self.locator_manager = locator_manager
self.chrome_window_name = "퍼센티 - 셀러들을 위한 AI 구매대행 솔루션 - Chrome" self.chrome_window_name = "퍼센티 - 셀러들을 위한 AI 구매대행 솔루션 - Chrome"
# self.whale_window_name = "새 시크릿 탭 - Whale" # self.whale_window_name = "새 시크릿 탭 - Whale"
self.chrome_hwnd = None self.chrome_hwnd = None
@ -20,6 +21,29 @@ class BrowserController:
self.browser = None self.browser = None
self.page = None self.page = None
# BrowserController에 해당하는 모든 locator를 정의
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.product_name_template = self.locator_manager.get_locator('BrowserControl', 'product_name_template')
self.product_price_template = self.locator_manager.get_locator('BrowserControl', 'product_price_template')
self.product_image_template = self.locator_manager.get_locator('BrowserControl', 'product_image_template')
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.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.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')
def get_page(self): def get_page(self):
return self.page return self.page
@ -62,19 +86,19 @@ class BrowserController:
if is_admin: if is_admin:
# 관리자 로그인 처리 # 관리자 로그인 처리
await self.page.fill('input[placeholder="이메일 주소 입력"]', admin_id) # 관리자 ID 입력 await self.page.fill(self.login_email_locator, admin_id) # 관리자 ID 입력
await self.page.fill('input[placeholder="영문/숫자/특수문자의 조합 (6~15자리)"]', admin_password) # 관리자 비밀번호 입력 await self.page.fill(self.login_password_locator, admin_password) # 관리자 비밀번호 입력
await self.page.click('button:has-text("로그인 하기")') # 관리자 로그인 버튼 클릭 await self.page.click(self.login_button_locator) # 관리자 로그인 버튼 클릭
else: else:
# 관리자 토글 버튼을 클릭해서 직원 로그인 화면 활성화 # 관리자 토글 버튼을 클릭해서 직원 로그인 화면 활성화
admin_toggle = self.page.locator('button[role="switch"]') admin_toggle = self.page.locator(self.admin_toggle_locator)
if await admin_toggle.get_attribute("aria-checked") == "true": if await admin_toggle.get_attribute("aria-checked") == "true":
await admin_toggle.click() # 관리자 모드에서 직원 모드로 전환 await admin_toggle.click() # 관리자 모드에서 직원 모드로 전환
await self.page.fill('input[placeholder="이메일 주소 입력"]', admin_id) # 관리자 ID 입력 await self.page.fill(self.login_email_locator, admin_id) # 관리자 ID 입력
await self.page.fill('input[placeholder="직원 아이디 입력"]', user_id) # 직원 ID 입력 await self.page.fill(self.staff_id_locator, user_id) # 직원 ID 입력
await self.page.fill('input[placeholder="영문/숫자/특수문자의 조합 (6~15자리)"]', user_password) # 직원 비밀번호 입력 await self.page.fill(self.login_password_locator, user_password) # 직원 비밀번호 입력
await self.page.click('button:has-text("직원 로그인 하기")') # 직원 로그인 버튼 클릭 await self.page.click(self.staff_login_button_locator) # 직원 로그인 버튼 클릭
self.logger.debug(f'로그인 완료: {"관리자" if is_admin else "직원"} 계정') self.logger.debug(f'로그인 완료: {"관리자" if is_admin else "직원"} 계정')
@ -82,7 +106,6 @@ class BrowserController:
await self.close_ad_if_exists() await self.close_ad_if_exists()
async def close_browser(self): async def close_browser(self):
"""브라우저 종료""" """브라우저 종료"""
if self.browser: if self.browser:
@ -161,40 +184,41 @@ class BrowserController:
def fetch_image_urls(self, html_content): def fetch_image_urls(self, html_content):
""" """
HTML 콘텐츠에서 모든 <img> 태그의 URL을 순서대로 추출 HTML 콘텐츠에서 모든 <img> 태그의 URL을 순서대로 추출하고 중복 제거.
""" """
soup = BeautifulSoup(html_content, 'html.parser') soup = BeautifulSoup(html_content, 'html.parser')
image_urls = []
# 중복된 이미지를 제거하기 위해 set 사용
image_urls_set = set()
# class="image_resized"를 가진 모든 <img> 태그 찾기 # class="image_resized"를 가진 모든 <img> 태그 찾기
images_resized = soup.find_all('img', class_='image_resized') images_resized = soup.find_all('img', class_='image_resized')
for img in images_resized: for img in images_resized:
if img and 'src' in img.attrs and img['src'] not in image_urls: if img and 'src' in img.attrs:
image_urls.append(img['src']) image_urls_set.add(img['src']) # 중복을 방지하기 위해 set에 추가
# <figure class="image"> 내부의 모든 <img> 태그 찾기 # <figure class="image"> 내부의 모든 <img> 태그 찾기
figures = soup.find_all('figure', class_='image') figures = soup.find_all('figure', class_='image')
for figure in figures: for figure in figures:
img_tag = figure.find('img') img_tag = figure.find('img')
if img_tag and 'src' in img_tag.attrs and img_tag['src'] not in image_urls: if img_tag and 'src' in img_tag.attrs:
image_urls.append(img_tag['src']) image_urls_set.add(img_tag['src']) # 중복을 방지하기 위해 set에 추가
# set을 list로 변환하여 반환 (순서 유지가 필요하면 set 대신 리스트로 처리해야 함)
image_urls = list(image_urls_set)
return image_urls return image_urls
async def close_ad_if_exists(self): async def close_ad_if_exists(self):
"""광고 다이얼로그가 있으면 닫기 버튼을 클릭하는 메서드""" """광고 다이얼로그가 있으면 닫기 버튼을 클릭하는 메서드"""
try: try:
# 광고 다이얼로그가 나타날 때까지 기다림 # 광고 다이얼로그가 나타날 때까지 기다림
dialog_selector = "div.ant-modal-wrap.ant-modal-centered" await self.page.wait_for_selector(self.close_ad_dialog_locator, timeout=3000)
close_button_selector = "div.ant-modal-footer > div > div > button[type='button'].ant-btn.css-1li46mu.ant-btn-default"
# 3초 동안 다이얼로그 대기
await self.page.wait_for_selector(dialog_selector, timeout=3000)
self.logger.debug("다이얼로그가 발견되었습니다. 닫기 버튼을 클릭합니다.") self.logger.debug("다이얼로그가 발견되었습니다. 닫기 버튼을 클릭합니다.")
# 닫기 버튼 클릭 # 닫기 버튼 클릭
close_button = await self.page.query_selector(close_button_selector) close_button = await self.page.query_selector(self.close_ad_button_locator)
if close_button: if close_button:
await close_button.click() await close_button.click()
self.logger.debug("다이얼로그를 성공적으로 닫았습니다.") self.logger.debug("다이얼로그를 성공적으로 닫았습니다.")
@ -208,7 +232,8 @@ class BrowserController:
async def go_to_new_product_page(self): async def go_to_new_product_page(self):
"""신규 상품 등록 페이지로 이동""" """신규 상품 등록 페이지로 이동"""
try: try:
await self.page.click('span.ant-menu-title-content:has-text("신규 상품 등록")') new_product_page_locator = self.locator_manager.get_locator('BrowserControl', 'new_product_page_locator')
await self.page.click(new_product_page_locator)
self.logger.debug("신규 상품 등록 페이지로 이동 완료.") self.logger.debug("신규 상품 등록 페이지로 이동 완료.")
except Exception as e: except Exception as e:
self.logger.debug(f"신규 상품 등록 페이지 이동 중 오류: {e}", exc_info=True) self.logger.debug(f"신규 상품 등록 페이지 이동 중 오류: {e}", exc_info=True)
@ -269,103 +294,59 @@ class BrowserController:
except Exception as e: except Exception as e:
self.logger.debug(f"옵션 탭 클릭 중 오류: {e}", exc_info=True) self.logger.debug(f"옵션 탭 클릭 중 오류: {e}", exc_info=True)
# async def extract_image_urls(self): async def click_price_tab(self):
# """HTML에서 이미지 URL 추출 및 img 태그 삭제 후 소스 버튼 다시 클릭""" """상세페이지 탭 클릭"""
# self.logger.debug('이미지 URL을 추출 중...') try:
await self.page.click('div.ant-tabs-tab:has-text("가격")')
# # 소스 버튼 클릭 self.logger.debug("가격 탭 클릭 완료.")
# await self.page.click("button[data-cke-tooltip-text='소스']") except Exception as e:
# self.logger.debug('소스 버튼 클릭 완료.') self.logger.debug(f"가격 탭 클릭 중 오류: {e}", exc_info=True)
# # 'data-value' 속성 값을 추출 (textarea 요소)
# textarea = await self.page.wait_for_selector('div.ck-source-editing-area')
# data_value = await textarea.get_attribute("data-value")
# if data_value:
# self.logger.debug('data-value 속성에서 HTML 수집 완료.')
# # 이미지 URL 추출
# image_urls = self.fetch_image_urls(data_value)
# self.logger.debug(f'추출된 이미지 URL 수: {len(image_urls)}')
# # 추출된 URL 반환
# self.logger.debug('img 태그를 삭제 중...')
# await self.page.wait_for_load_state('domcontentloaded') # 페이지 로딩 완료 대기
# # data-value 속성을 가진 요소 선택
# data_value_element = await self.page.query_selector('div.ck-source-editing-area')
# new_value = ""
# if data_value_element:
# # 속성 변경 (원하는 텍스트로 변경하거나 ""으로 변경)
# # self.page.evaluate('(element, value) => element.setAttribute("data-value", value)', data_value_element, new_value)
# await self.page.evaluate(f'() => document.querySelector("div.ck-source-editing-area").setAttribute("data-value", "{new_value}")')
# # 데이터가 제대로 변경되었는지 확인
# updated_value = await data_value_element.get_attribute('data-value')
# self.logger(f'Updated data-value: {updated_value}')
# else:
# self.logger('Element with data-value not found.')
# self.logger.debug('img 태그 삭제 완료.')
# # 소스 버튼 다시 클릭
# await self.page.click("button[data-cke-tooltip-text='소스']")
# self.logger.debug('소스 버튼 재 클릭 완료.')
# return image_urls
# else:
# self.logger.debug('data-value 속성에 데이터가 없습니다.')
# return []
async def extract_image_urls(self, optionHandler, is_option_data=False): async def extract_image_urls(self, optionHandler, is_option_data=False):
"""상세페이지에서 이미지 URL 추출""" """상세페이지에서 이미지 URL 추출"""
try: try:
# 소스 편집 모드로 전환 # 소스 편집 모드로 전환
await self.page.click('button[data-cke-tooltip-text="소스"]') 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("소스 버튼 클릭 완료.") self.logger.debug("소스 버튼 클릭 완료.")
# 'data-value' 속성 값을 추출 (textarea 요소) # 'data-value' 속성 값을 추출 (textarea 요소)
textarea = await self.page.wait_for_selector('div.ck-source-editing-area') textarea = await self.page.wait_for_selector(ck_source_editing_area_locator)
data_value = await textarea.get_attribute("data-value") data_value = await textarea.get_attribute("data-value")
# HTML 소스에서 이미지 URL 추출 # HTML 소스에서 이미지 URL 추출
image_urls = self.fetch_image_urls(data_value) image_urls = self.fetch_image_urls(data_value)
self.logger.debug(f'추출된 이미지 URL 수: {len(image_urls)}') self.logger.debug(f'추출된 이미지 URL 수: {len(image_urls)}')
# HTML 소스에서 이미지 URL 삭제 # HTML 소스에서 이미지 URL 삭제
self.logger.debug('img 태그를 삭제 중...') self.logger.debug('img 태그를 삭제 중...')
# await self.page.wait_for_load_state('domcontentloaded') # 페이지 로딩 완료 대기 data_value_element = await self.page.query_selector(ck_source_editing_area_locator)
# data-value 속성을 가진 요소 선택
data_value_element = await self.page.query_selector('div.ck-source-editing-area')
new_value = "" new_value = ""
if data_value_element: if data_value_element:
# 속성 변경 (원하는 텍스트로 변경하거나 ""으로 변경) await self.page.evaluate(f'() => document.querySelector("{ck_source_editing_area_locator}").setAttribute("data-value", "{new_value}")')
# self.page.evaluate('(element, value) => element.setAttribute("data-value", value)', data_value_element, new_value)
await self.page.evaluate(f'() => document.querySelector("div.ck-source-editing-area").setAttribute("data-value", "{new_value}")')
# 데이터가 제대로 변경되었는지 확인
updated_value = await data_value_element.get_attribute('data-value') updated_value = await data_value_element.get_attribute('data-value')
self.logger.debug(f'Updated data-value: {updated_value}') self.logger.debug(f'Updated data-value: {updated_value}')
else: else:
self.logger.debug('Element with data-value not found.') self.logger.debug('Element with data-value not found.')
self.logger.debug('img 태그 삭제 완료.') self.logger.debug('img 태그 삭제 완료.')
# img 태그의 class 삭제 후 다시 소스 버튼 클릭 # img 태그의 class 삭제 후 다시 소스 버튼 클릭
await self.page.click('button[data-cke-tooltip-text="소스"]') await self.page.click(source_button_locator)
self.logger.debug('소스 버튼 재 클릭 완료.') self.logger.debug('소스 버튼 재 클릭 완료.')
if is_option_data: if is_option_data:
self.logger.debug('옵션 데이터 입력 시작') self.logger.debug('옵션 데이터 입력 시작')
option_data = optionHandler.get_selected_translated_options() option_data = optionHandler.get_selected_translated_options()
# 옵션 입력 필드 선택 # 옵션 입력 필드 선택
input_field_selector = '//*[@id="productMainContentContainerId"]/div/div/div[2]/div[2]/div[2]/div' # 여기에 적절한 입력 필드의 셀렉터를 입력 input_field_locator = self.locator_manager.get_locator('BrowserControl', 'option_input_field_locator')
input_field = await self.page.wait_for_selector(input_field_selector) input_field = await self.page.wait_for_selector(input_field_locator)
# 선두부 텍스트 입력 # 선두부 텍스트 입력
await input_field.type('---') await input_field.type('---')
@ -446,7 +427,8 @@ class BrowserController:
"""다음 페이지로 이동""" """다음 페이지로 이동"""
try: try:
# 현재 페이지가 몇 번째 페이지인지 확인 (클래스에 'ant-pagination-item-active'가 있는 요소) # 현재 페이지가 몇 번째 페이지인지 확인 (클래스에 'ant-pagination-item-active'가 있는 요소)
current_page = await self.page.query_selector('li.ant-pagination-item.ant-pagination-item-active') current_page_locator = self.locator_manager.get_locator('BrowserControl', 'current_page_locator')
current_page = await self.page.query_selector(current_page_locator)
if not current_page: if not current_page:
self.logger.debug("현재 페이지 정보를 찾을 수 없습니다.") self.logger.debug("현재 페이지 정보를 찾을 수 없습니다.")
@ -457,7 +439,9 @@ class BrowserController:
next_page_number = current_page_number + 1 next_page_number = current_page_number + 1
# 다음 페이지 버튼을 찾음 (title 속성으로 다음 페이지를 찾음) # 다음 페이지 버튼을 찾음 (title 속성으로 다음 페이지를 찾음)
next_page_button = await self.page.query_selector(f'li.ant-pagination-item[title="{next_page_number}"]') next_page_button_template = self.locator_manager.get_locator('BrowserControl', 'next_page_button_template')
next_page_button_locator = 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: if next_page_button:
await next_page_button.click() # 페이지 버튼 클릭 await next_page_button.click() # 페이지 버튼 클릭
@ -481,16 +465,11 @@ class BrowserController:
win32gui.ShowWindow(self.chrome_hwnd, win32con.SW_RESTORE) win32gui.ShowWindow(self.chrome_hwnd, win32con.SW_RESTORE)
win32gui.SetForegroundWindow(self.chrome_hwnd) win32gui.SetForegroundWindow(self.chrome_hwnd)
self.logger.debug('크롬 창으로 포커스 이동.') self.logger.debug('크롬 창으로 포커스 이동.')
self.logger.debug('크롬 창으로 포커스 이동.')
else: else:
self.logger.debug('크롬 창을 찾을 수 없습니다.') self.logger.debug('크롬 창을 찾을 수 없습니다.')
self.logger.debug('크롬 창을 찾을 수 없습니다.')
except Exception as e: except Exception as e:
self.logger.debug(f"크롬 포커스 전환 중 오류: {e}", exc_info=True) self.logger.debug(f"크롬 포커스 전환 중 오류: {e}", exc_info=True)
async def scroll_with_wheel(self, direction="down", pause_time=0.5, max_scrolls=50): async def scroll_with_wheel(self, direction="down", pause_time=0.5, max_scrolls=50):
""" """
스크롤을 사용하여 페이지를 위나 아래로 천천히 스크롤. 스크롤을 사용하여 페이지를 위나 아래로 천천히 스크롤.
@ -585,13 +564,13 @@ class BrowserController:
for i in range(1, 51): # 1부터 최대 50까지 상품 처리 for i in range(1, 51): # 1부터 최대 50까지 상품 처리
try: try:
# 각 상품의 CSS 선택자를 동적으로 생성하여 접근 # 각 상품의 CSS 선택자를 동적으로 생성하여 접근
product_name_selector = f"div#root div:nth-child({i}) > div > li > div > div > div:nth-child(2) > div > div > div.ant-col.css-1li46mu > div.sc-dPZUQH.mXDuy > span.sc-dSIIpw.Nrwqu.Body3Regular14.CharacterPrimary85" product_name_locator = self.product_name_template.format(i=i)
product_price_selector = f"div#root div:nth-child({i}) > div > li > div > div > div:nth-child(2) > div > div > div.ant-col.css-1li46mu > div:nth-child(3) > div:nth-child(1) > span.sc-dSIIpw.Nrwqu.Body3Regular14.CharacterPrimary85" product_price_locator = self.product_price_template.format(i=i)
product_image_selector = f"div#root div:nth-child({i}) > div > li > div > div > div:nth-child(1) > div > div:nth-child(2) > div > div > img" product_image_locator = self.product_image_template.format(i=i)
product_name_element = self.page.locator(product_name_selector) product_name_element = await self.page.query_selector(product_name_locator)
product_price_element = self.page.locator(product_price_selector) product_price_element = await self.page.query_selector(product_price_locator)
product_image_element = self.page.locator(product_image_selector) product_image_element = await self.page.query_selector(product_image_locator)
if product_name_element and product_price_element and product_image_element: if product_name_element and product_price_element and product_image_element:
product_info = { product_info = {

View File

@ -11,6 +11,7 @@ import os
from datetime import datetime from datetime import datetime
import random import random
import asyncio import asyncio
import pyperclip # 클립보드 데이터를 확인하기 위한 라이브러리
class ClipboardImageManager: class ClipboardImageManager:
def __init__(self, app, logger, browser_controller, debug=False): def __init__(self, app, logger, browser_controller, debug=False):

87
config.ini Normal file
View File

@ -0,0 +1,87 @@
[PriceLocators]
return_fee_input_locator = //*[@id='productMainContentContainerId']/div/div[1]/div/div/div[4]/div/div[1]/div[3]/div/div/div/div[1]/div[2]/input
first_delv_fee_input_locator = //*[@id='productMainContentContainerId']/div/div[1]/div/div/div[4]/div/div[1]/div[4]/div/div[2]/div/div[1]/div[2]/input
exchange_fee_input_locator = //*[@id='productMainContentContainerId']/div/div[1]/div/div/div[4]/div/div[1]/div[5]/div/div/div/div[1]/div[2]/input
plus_margin_locator = //*[@id='productMainContentContainerId']/div/div[1]/div/div/div[2]/div/div[1]/div[8]/div/div/div[3]/div/div/div/div[1]/div[2]/input
oversea_shipping_locator = //*[@id='productMainContentContainerId']/div/div[1]/div/div/div[2]/div/div[1]/div[10]/div/div/div/div[1]/div[2]/input
option_count_text_locator = 'div#productMainContentContainerId th:nth-child(2) > div > span'
product_cost_locator = //*[@id='productMainContentContainerId']/div/div[2]/div/div/div[5]/div[1]/div/div/div/div/div[2]/table/tbody/tr[{i}]/td[3]/div/div/div/div[2]/input
standard_selling_price_locator = //*[@id='productMainContentContainerId']/div/div[2]/div/div/div[5]/div[1]/div/div/div/div/div[2]/table/tbody/tr[{i}]/td[4]/div/div/div[1]/div/div[2]/input
[OptionLocators]
option_input_locator = //*[@id='optionMainContainerId']/div/div[2]/table/tbody/tr[{i}]/td[2]/input
option_price_locator = //*[@id='optionMainContainerId']/div/div[2]/table/tbody/tr[{i}]/td[4]/input
[DetailLocators]
product_detail_input_locator = //*[@id='detailMainContainerId']/div/div/div[{i}]/textarea
product_image_locator = //*[@id='detailMainContainerId']/div/div/div[{i}]/img
[ProductNameLocators]
product_name_input_locator = //*[@id='productMainContentContainerId']/div/div[1]/div/div/div[1]/input
[BrowserControl]
# 크롬 창 이름
chrome_window_name = 퍼센티 - 셀러들을 위한 AI 구매대행 솔루션 - Chrome
# 로그인 관련 선택자
admin_id_input = input[placeholder="이메일 주소 입력"]
admin_password_input = input[placeholder="영문/숫자/특수문자의 조합 (6~15자리)"]
admin_login_button = button:has-text("로그인 하기")
user_id_input = input[placeholder="직원 아이디 입력"]
user_password_input = input[placeholder="영문/숫자/특수문자의 조합 (6~15자리)"]
user_login_button = button:has-text("직원 로그인 하기")
admin_toggle_button = button[role="switch"]
# 광고 다이얼로그
ad_dialog_selector = div.ant-modal-wrap.ant-modal-centered
ad_close_button_selector = div.ant-modal-footer > div > div > button[type='button'].ant-btn.css-1li46mu.ant-btn-default
# 상품 수 관련 선택자
total_product_count = '#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)'
# 상품명 관련 선택자
product_name_xpath = //div[{index}]/div/li/div/div/div[2]/div/div/div[1]/div[1]/span[2]
product_name_css = 'div#root div:nth-child({index}) > div > li > div > div > div:nth-child(2) > div > div > div.ant-col.css-1li46mu > div.sc-ktPPKK.ezbvYT > span.sc-ecPEgm.gmiQgL.Body3Regular14.CharacterPrimary85'
# 상품 수정을 위한 버튼
product_edit_button = button:has-text("세부사항 수정 및 업로드")
# 탭 관련 선택자
detail_tab = div.ant-tabs-tab:has-text("상세페이지")
option_tab = div.ant-tabs-tab:has-text("옵션")
price_tab = div.ant-tabs-tab:has-text("가격")
# 페이지 이동 관련 선택자
next_page_button_template = li.ant-pagination-item[title="{page_number}"]
new_product_page_locator = span.ant-menu-title-content:has-text("신규 상품 등록")
current_page_locator = li.ant-pagination-item.ant-pagination-item-active
# 이미지 URL 추출 관련 선택자
source_button = button[data-cke-tooltip-text="소스"]
source_editing_area = div.ck-source-editing-area
# 상세페이지 소스 관련 선택자
source_button_locator = button[data-cke-tooltip-text="소스"]
ck_source_editing_area_locator = div.ck-source-editing-area
option_input_field_locator = 'div#productMainContentContainerId > div > div > div:nth-child(2) > div:nth-child(2) > div:nth-child(2) > div'
[CategoryMargins]
categories = 가구, 농기구
[가구]
threshold_1 = 100000 # 10만원
extra_margin_1 = 10000 # 2만원 초과마다 1만원 추가
unit_1 = 20000
threshold_2 = 200000 # 20만원
extra_margin_2 = 15000 # 2만원 초과마다 1.5만원 추가
unit_2 = 20000
[농기구]
threshold_1 = 100000 # 10만원
extra_margin_1 = 5000 # 2만원 초과마다 0.5만원 추가
unit_1 = 20000
threshold_2 = 200000 # 20만원
extra_margin_2 = 10000 # 2만원 초과마다 1만원 추가
unit_2 = 20000

33
gui.py
View File

@ -6,6 +6,8 @@ from whale_translator import WhaleTranslator
from clipboardImageManager import ClipboardImageManager from clipboardImageManager import ClipboardImageManager
from vertexAI import VertexAITranslator from vertexAI import VertexAITranslator
from option import OptionHandler from option import OptionHandler
from price import PriceHandler
from locatorManager import LocatorManager
from logger_module import QTextEditLogger # 추가 from logger_module import QTextEditLogger # 추가
import logging import logging
import asyncio import asyncio
@ -18,7 +20,8 @@ class TranslationApp(QWidget):
self.debug = False self.debug = False
key_path = 'leensoo1nt.json' key_path = 'leensoo1nt.json'
self.settings = QSettings("WhenRideMycar", "TranslationApp") # QSettings 초기화 self.settings = QSettings("WhenRideMycar", "TranslationApp") # QSettings 초기화
self.browser_controller = BrowserController(self, self.logger) self.locator_manager = LocatorManager()
self.browser_controller = BrowserController(self, self.logger, self.locator_manager)
# self.whale_translator = WhaleTranslator(self, self.logger, secret_mode=True,vd_mode=True) # 디버그 모드 켜기 # self.whale_translator = WhaleTranslator(self, self.logger, secret_mode=True,vd_mode=True) # 디버그 모드 켜기
self.whale_translator = whale_translator self.whale_translator = whale_translator
@ -26,7 +29,8 @@ class TranslationApp(QWidget):
self.optionHandler = None self.optionHandler = None
self.clipboardImageManager = ClipboardImageManager(self, logger, self.browser_controller, self.debug) self.clipboardImageManager = ClipboardImageManager(self, logger, self.browser_controller, self.debug)
self.optionHandler = OptionHandler(self.browser_controller, self.whale_translator, self.logger, self.vertexAI, self.debug) self.optionHandler = OptionHandler(self.locator_manager, self.browser_controller, self.whale_translator, self.logger, self.vertexAI, self.debug)
self.priceHandler = PriceHandler(self.locator_manager, self.browser_controller, self.logger, self.vertexAI, self.debug)
self.running = False self.running = False
@ -406,6 +410,11 @@ class TranslationApp(QWidget):
await self.edit_option(product_name) await self.edit_option(product_name)
self.complete_stage(0) self.complete_stage(0)
# 가격 수정
# self.start_stage(0)
await self.edit_price()
# self.complete_stage(0)
# 상세페이지 수정 # 상세페이지 수정
self.start_stage(1) self.start_stage(1)
await self.detail_trans() await self.detail_trans()
@ -580,12 +589,28 @@ class TranslationApp(QWidget):
self.detail_progress_bar.setVisible(True) self.detail_progress_bar.setVisible(True)
# 옵션 최대선택갯수 # 옵션 최대선택갯수
max_option_count = 10 max_option_count = 20
option_image_trans = False option_image_trans = False
await self.optionHandler.process_options(product_name, max_option_count, option_image_trans) await self.optionHandler.process_options(product_name, max_option_count, option_image_trans)
# 수정 후 저장 # 수정 후 저장
await self.optionHandler.save_option() # await self.optionHandler.save_option()
await self.browser_controller.save_product_edit()
self.detail_progress_bar.setVisible(False) self.detail_progress_bar.setVisible(False)
async def edit_price(self):
# 상세페이지 탭 클릭
await self.browser_controller.click_option_tab()
# await self.browser_controller.page.wait_for_load_state('networkidle', timeout=10000)
self.detail_progress_bar.setVisible(True)
# 가격 수정 프로세스
await self.priceHandler.process_price()
# 수정 후 저장
await self.browser_controller.save_product_edit()
self.detail_progress_bar.setVisible(False)

49
locatorManager.py Normal file
View File

@ -0,0 +1,49 @@
import configparser
class LocatorManager:
def __init__(self, config_file='config.ini'):
self.config_file = config_file
self.selectors = {}
self.load_locators_from_config()
def load_locators_from_config(self):
"""
config.ini 파일에서 선택자를 불러와 self.selectors에 저장
"""
config = configparser.ConfigParser()
config.read(self.config_file)
# PriceLocators 섹션
self.selectors['PriceLocators'] = {
'return_fee_input_locator': config.get('PriceLocators', 'return_fee_input_locator'),
'first_delv_fee_input_locator': config.get('PriceLocators', 'first_delv_fee_input_locator'),
'exchange_fee_input_locator': config.get('PriceLocators', 'exchange_fee_input_locator'),
'plus_margin_locator': config.get('PriceLocators', 'plus_margin_locator'),
'oversea_shipping_locator': config.get('PriceLocators', 'oversea_shipping_locator'),
'option_count_text_locator': config.get('PriceLocators', 'option_count_text_locator'),
'product_cost_locator': config.get('PriceLocators', 'product_cost_locator'),
'standard_selling_price_locator': config.get('PriceLocators', 'standard_selling_price_locator')
}
# OptionLocators 섹션
self.selectors['OptionLocators'] = {
'option_input_locator': config.get('OptionLocators', 'option_input_locator'),
'option_price_locator': config.get('OptionLocators', 'option_price_locator')
}
def get_locator(self, section, key):
"""
섹션과 키를 받아서 해당 선택자를 반환
"""
return self.selectors.get(section, {}).get(key, None)
def get_category_data(self, section, category):
"""
Config.ini에서 특정 카테고리에 대한 데이터를 반환하는 메서드
"""
try:
category_data = self.config[section][category]
return category_data
except KeyError:
self.logger.error(f"{section} 섹션에서 {category} 카테고리를 찾을 수 없습니다.")
return None

View File

@ -48,7 +48,7 @@ async def main():
logger.error(f"DPI 인식 설정 실패: {e}") logger.error(f"DPI 인식 설정 실패: {e}")
# 기존 TranslationApp UI 사용 # 기존 TranslationApp UI 사용
whale_translator = WhaleTranslator(app, logger, secret_mode=True, vd_mode=True) # 디버그 모드 켜기 whale_translator = WhaleTranslator(app, logger, secret_mode=True, vd_mode=True) # 모드 켜기
await whale_translator.start_whale_browser() await whale_translator.start_whale_browser()
window = TranslationApp(logger, whale_translator) # PySide6 UI window = TranslationApp(logger, whale_translator) # PySide6 UI
window.show() window.show()

View File

@ -5,7 +5,8 @@ import numpy as np
import asyncio import asyncio
class OptionHandler: class OptionHandler:
def __init__(self, browser_controller, whale_translator, logger, vertexAI, debug_flag=False): def __init__(self, locator_manager, browser_controller, whale_translator, logger, vertexAI, debug_flag=False):
self.locator_manager = locator_manager
self.browser_controller = browser_controller self.browser_controller = browser_controller
self.page = self.browser_controller.page self.page = self.browser_controller.page
self.logger = logger self.logger = logger

760
price.py Normal file
View File

@ -0,0 +1,760 @@
from collections import Counter
import pyautogui
from datetime import datetime
import numpy as np
import asyncio
import math , re, time
from playwright.async_api import TimeoutError
class PriceHandler:
def __init__(self, locator_manager, browser_controller, logger, vertexAI, debug_flag=False):
self.locator_manager = locator_manager
self.browser_controller = browser_controller
self.page = self.browser_controller.page
self.logger = logger
self.debug_flag = debug_flag
self.vertexAItranslator = vertexAI
# Locator들을 미리 로드하여 초기화 - locator_template:동적 로딩, locator:고정로딩
self.product_cost_locator_template = self.locator_manager.get_locator('PriceLocators', 'product_cost_locator')
self.standard_selling_price_locator_template = self.locator_manager.get_locator('PriceLocators', 'standard_selling_price_locator')
self.plus_margin_locator = self.locator_manager.get_locator('PriceLocators', 'plus_margin_locator')
self.oversea_shipping_locator = self.locator_manager.get_locator('PriceLocators', 'oversea_shipping_locator')
self.return_fee_input_locator = self.locator_manager.get_locator('PriceLocators', 'return_fee_input_locator')
self.first_delv_fee_input_locator = self.locator_manager.get_locator('PriceLocators', 'first_delv_fee_input_locator')
self.exchange_fee_input_locator = self.locator_manager.get_locator('PriceLocators', 'exchange_fee_input_locator')
self.option_count_text_locator = self.locator_manager.get_locator('PriceLocators', 'option_count_text_locator')
self.initial_setting()
self.initialize_values() # 초기값 설정 메서드
def initialize_values(self):
"""
가격과 관련된 변수들을 초기화하는 메서드입니다.
여러 상품을 처리할 데이터가 이전 상품에 영향을 주지 않도록 초기화합니다.
"""
self.shipping_config = None
self.margin_config = None
self.optimal_price_config = None
self.product_costs = []
self.option_data = []
self.sold_price = 0
self.calculated_price = 0
self.optimal_price = 0
self.return_fee = 0
self.first_delv_fee = 0
self.exchange_fee = 0
self.sold_price = 0
def initial_setting(self):
# 설정 초기화
self.shipping_config = self.set_shipping_config(min_price=50000, thresholds=[50000, 100000, 200000], increment_unit=20000, additional_costs=[5000, 7000, 9000])
self.margin_config = self.set_margin_config(thresholds=[50000, 70000, 100000, 150000, 200000, 300000, 400000, 500000, 1000000], additional_margins=[5000, 10000, 15000, 25000, 35000, 50000, 70000, 90000, 120000])
self.optimal_price_config = self.set_optimal_price_config(sold_price=None, cost_price2X=None, calculated_price=None, lower_bound=0.85, upper_bound=1.15, ratios={'sold_price': 0.5, 'cost_price2X': 0.3, 'calculated_price': 0.2})
async def process_price(self, sold_price=0, category=None):
try:
# 상품정보 초기화
self.initialize_values()
# 1. 페이지에서 가격 정보 수집
self.logger.debug("초기 더하기마진과 해외배송비 가격 정보를 수집합니다.")
initial_plusmargin, initial_shipping_price = await self.get_plusmargin_and_shipping_values()
# if initial_plusmargin and initial_plusmargin != 10000:
if (initial_plusmargin != 10000 and initial_shipping_price != 0):
sold_price = initial_plusmargin
self.logger.debug(f"더하기마진값{initial_plusmargin}을 팔린가격{sold_price}으로 간주")
self.logger.debug("옵션 가격 정보를 수집합니다.")
option_data, min_cost, max_cost, avg_cost, upper_avg_cost = await self.collect_product_costs_and_prices() # 수집된 옵션정보를 반환
if option_data is None:
self.logger.error("상품 옵션 정보를 수집하지 못했습니다.", exc_info=True)
return
# 2. 원가 기반가격 계산
calculated_price = self.calc_initial_price(upper_avg_cost, self.margin_config, self.shipping_config, 0.04, 0.24)
# 3. 적정 판매가 산출
self.logger.debug("적정 판매가를 계산합니다.")
self.optimal_price_config['sold_price'] = sold_price # 팔린가격 기본값(10000이 아닌 값이 있을 경우 팔린가격으로 간주)
self.optimal_price_config['cost_price2X'] = upper_avg_cost # 원가2배 가격 : upper_avg_cost를 기준으로 계산.
self.optimal_price_config['calculated_price'] = calculated_price # 기준판매가를 기준으로 계산된 값
optimal_price = self.calculate_optimal_price(self.optimal_price_config)
self.logger.debug(f"계산된 적정 판매가: {optimal_price}")
# 4. 더하기 마진을 적정 판매가 기준으로 재설정
self.logger.debug("더하기 마진을 적정 판매가에 맞게 조정합니다.")
additional_margin = self.calculate_adjusted_margin(optimal_price, option_data, self.margin_config)
additional_margin = self.round_to_UP(additional_margin)
self.logger.debug(f"조정된 더하기 마진: {additional_margin}")
# 5. 해외 배송비 재계산
shipping_base_price = optimal_price - upper_avg_cost - (upper_avg_cost*0.04) - (upper_avg_cost*0.24) - additional_margin
shipping_cost = self.calculate_shipping_cost_with_extended_thresholds(10000, shipping_base_price, self.shipping_config)
shipping_cost = self.round_to_UP(shipping_cost)
self.logger.debug(f"적정판매가 기준으로 재계산된 해외배송비: {shipping_cost}")
# 5. 카테고리별 추가 해외배송비 계산
extra_shipping = self.calculate_category_extra_shipping(category, optimal_price)
shipping_cost += extra_shipping
self.logger.debug(f"카테고리별 추가배송비 기준으로 재계산된 해외배송비: {shipping_cost}")
# 5. 계산된 값 입력
self.logger.debug("계산된 값을 페이지에 입력합니다.")
await self.input_calculated_values(additional_margin, shipping_cost)
# 6. 반품비, 초도배송비, 교환비 계산 및 입력
return_fee, first_delv_fee, exchange_fee = self.calculate_claim_costs(max_cost)
self.logger.debug(f"반품비: {return_fee}, 초도배송비: {first_delv_fee}, 교환비: {exchange_fee}")
await self.input_claim_costs(return_fee, first_delv_fee, exchange_fee)
# 7. 저장
# save_xpath = "//button[contains(.,'저장하기')]"
# click_element(driver, 'XPATH', save_xpath, 10, 'js')
# self.logger.debug("가격 정리 후 저장버튼 클릭 완료")
except Exception as e:
self.logger.error(f"가격 수정 중 오류 발생: {e}", exc_info=True)
async def input_claim_costs(self, return_fee, first_delv_fee, exchange_fee):
"""
계산된 반품비, 초도배송비, 교환비를 입력합니다.
Parameters:
- driver: WebDriver 인스턴스
- return_fee (int): 반품비
- first_delv_fee (int): 초도배송비
- exchange_fee (int): 교환비
"""
try:
# 반품비 수정
return_fee_element = await self.page.wait_for_selector(self.return_fee_input_locator)
if return_fee_element:
return_fee = self.round_to_UP(return_fee)
await return_fee_element.fill(str(return_fee)) # 기존 내용을 지우고 새 값을 입력
self.logger.debug(f"반품비 수정 완료: {return_fee}")
else:
self.logger.error(f"반품비 입력 중 오류 발생: 요소를 찾을 수 없음", exc_info=True)
# 초도배송비 수정
first_delv_fee_element = await self.page.wait_for_selector(self.first_delv_fee_input_locator)
if first_delv_fee_element:
first_delv_fee = self.round_to_UP(first_delv_fee)
await first_delv_fee_element.fill(str(first_delv_fee)) # 기존 내용을 지우고 새 값을 입력
self.logger.debug(f"초도배송비 수정 완료: {first_delv_fee}")
else:
self.logger.error(f"초도배송비 입력 중 오류 발생: 요소를 찾을 수 없음", exc_info=True)
# 교환비 수정
exchange_fee_element = await self.page.wait_for_selector(self.exchange_fee_input_locator)
if exchange_fee_element:
exchange_fee = self.round_to_UP(exchange_fee)
await exchange_fee_element.fill(str(exchange_fee)) # 기존 내용을 지우고 새 값을 입력
self.logger.debug(f"교환비 수정 완료: {exchange_fee}")
else:
self.logger.error(f"교환비 입력 중 오류 발생: 요소를 찾을 수 없음", exc_info=True)
except Exception as e:
self.logger.error(f"Claim cost 수정 중 오류 발생: {e}", exc_info=True)
def calculate_claim_costs(self, max_cost: int) -> tuple[int, int, int]:
"""
반품비, 초도배송비, 교환비를 계산합니다.
Parameters:
- max_cost (int): 상품의 최대 원가
Returns:
- return_fee (int): 반품비
- first_delv_fee (int): 초도배송비
- exchange_fee (int): 교환비
"""
try:
max_return_fee = 199000
max_exchange_fee = 499000
return_fee = min(max_return_fee, max_cost)
return_fee = self.round_to_UP(return_fee)
first_delv_fee = return_fee # 초도배송비는 반품비와 동일
exchange_fee = min(max_exchange_fee, return_fee * 2)
exchange_fee = self.round_to_UP(exchange_fee)
self.logger.debug(f"계산된 반품비: {return_fee}, 초도배송비: {first_delv_fee}, 교환비: {exchange_fee}")
return return_fee, first_delv_fee, exchange_fee
except Exception as e:
self.logger.error(f"Claim cost 계산 중 오류 발생: {e}", exc_info=True)
return 199000, 199000, 499000
async def input_calculated_values(self, margin, shipping_cost):
"""
계산된 마진, 배송비, 교환비를 옵션에 입력합니다.
Parameters:
- driver: WebDriver 인스턴스
- margin (int): 더하기 마진
- shipping_cost (int): 해외배송비
"""
try:
# 1000원 단위 절상
margin = self.round_to_UP(margin)
shipping_cost = self.round_to_UP(shipping_cost)
# 더하기 마진 입력 (4번째 인풋박스)
margin_element = await self.page.wait_for_selector(self.plus_margin_locator)
await margin_element.fill(str(margin))
self.logger.debug(f"더하기 마진 입력 완료: {margin}")
# 해외 배송비 입력 (5번째 인풋박스)
oversea_shipping_element = await self.page.wait_for_selector(self.oversea_shipping_locator)
await oversea_shipping_element.fill(str(shipping_cost))
self.logger.debug(f"해외 배송비 입력 완료: {shipping_cost}")
except Exception as e:
self.logger.error(f"Failed to input calculated values: {e}", exc_info=True)
def calculate_shipping_cost_with_extended_thresholds(self, base_cost, product_price, config, max_threshold=5000000):
"""
점진적으로 증가하는 해외배송비를 계산하는 메서드입니다.
Parameters:
- base_cost (int): 기본 해외배송비.
- product_price (int): 상품의 가격.
- config (dict): 구간과 추가 배송비 정보를 포함하는 딕셔너리.
- max_threshold (int): 추가 구간을 자동 확장할 최대 가격 (기본값: 5000000).
Returns:
- total_shipping_cost (int): 계산된 해외배송비.
"""
try:
total_shipping_cost = base_cost
min_price_for_extra_shipping = config.get('min_price_for_extra_shipping', 0)
thresholds = config.get('thresholds', [])
increment_unit = config.get('increment_unit', 0)
additional_costs = config.get('additional_costs', [])
if not thresholds or not additional_costs:
raise ValueError("기준 구간과 추가 비용 정보는 비워둘 수 없습니다.")
self.logger.debug(f"기존 구간: {thresholds}")
self.logger.debug(f"기존 추가 비용: {additional_costs}")
# 주어진 product_price 까지만 확장하도록 변경
while thresholds[-1] < max_threshold and thresholds[-1] < product_price:
last_threshold_diff = thresholds[-1] - thresholds[-2]
new_threshold_diff = last_threshold_diff + (last_threshold_diff - (thresholds[-2] - thresholds[-3]) if len(thresholds) > 2 else last_threshold_diff)
last_cost_diff = additional_costs[-1] - additional_costs[-2]
new_cost_diff = last_cost_diff + (last_cost_diff - (additional_costs[-2] - additional_costs[-3]) if len(additional_costs) > 2 else last_cost_diff)
next_threshold = thresholds[-1] + new_threshold_diff
next_additional_cost = additional_costs[-1] + new_cost_diff
thresholds.append(next_threshold)
additional_costs.append(next_additional_cost)
self.logger.debug(f"확장된 '해외배송비' 구간: {next_threshold}")
self.logger.debug(f"확장된 '해외배송비' 추가 비용: {next_additional_cost}")
# 상품 가격이 최소 추가 배송비 적용 금액 이상일 때만 추가 비용을 계산
if product_price > min_price_for_extra_shipping:
remaining_price = product_price - min_price_for_extra_shipping
for i in range(len(thresholds)):
if remaining_price > 0:
if i < len(thresholds) - 1:
next_threshold = thresholds[i + 1]
if remaining_price <= next_threshold - min_price_for_extra_shipping:
increments = remaining_price // increment_unit
total_shipping_cost += increments * additional_costs[i]
self.logger.debug(f"적용된 구간: {next_threshold}, 계산된 구간추가배송비: {increments * additional_costs[i]}")
remaining_price = 0
else:
increments = (next_threshold - min_price_for_extra_shipping) // increment_unit
total_shipping_cost += increments * additional_costs[i]
self.logger.debug(f"적용된 구간: {next_threshold}, 계산된 구간추가배송비: {increments * additional_costs[i]}")
remaining_price -= next_threshold - min_price_for_extra_shipping
min_price_for_extra_shipping = next_threshold
else:
increments = remaining_price // increment_unit
total_shipping_cost += increments * additional_costs[i]
self.logger.debug(f"최대 구간 적용, 계산된 구간추가배송비: {increments * additional_costs[i]}")
remaining_price = 0
result = self.round_to_UP(total_shipping_cost)
self.logger.debug(f"최종 계산된 배송비: {result}")
return result
except ValueError as ve:
self.logger.error(f"기본 구성 오류: {ve}", exc_info=True)
return base_cost # 예외 발생 시 기본 배송비만 반환
except Exception as e:
self.logger.error(f"계산된 값 입력 실패: {e}", exc_info=True)
return max(base_cost, 10000) # 예외 발생 시 최소 기본 배송비 반환
def calculate_adjusted_margin(self, optimal_price, option_data, margin_config):
"""
적정판매가와 기준판매가를 비교하여 더하기 마진을 조정하는 메서드입니다.
Parameters:
- optimal_price (int): 계산된 적정 판매가.
- option_data (list): 옵션의 데이터.
- margin_config (dict): 더하기 마진 설정 딕셔너리.
Returns:
- adjusted_margin (int): 적정 판매가에 맞게 조정된 더하기 마진.
"""
try:
total_option_price = math.ceil(sum([option['price'] for option in option_data]) / len(option_data))
self.logger.debug(f"총 옵션 기준 판매가 평균: {total_option_price}")
# 기준판매가와 적정판매가의 차이를 바탕으로 마진을 조정
price_difference = optimal_price - total_option_price
self.logger.debug(f"적정 판매가와 기준 판매가 차이: {price_difference}")
# 마진 설정: 차이에 따른 더하기 마진 재조정
adjusted_margin = self.calculate_additional_margin_with_extended_thresholds(optimal_price, margin_config)
# 만약 차이가 크다면, 추가로 마진을 더 조정할 수 있음
if price_difference > 0:
adjusted_margin += price_difference * 0.1 # 차이의 10%를 추가 마진에 반영
return adjusted_margin
except Exception as e:
self.logger.error(f"더하기 마진 조정 중 오류 발생: {e}", exc_info=True)
return 0 # 기본값 반환
def calculate_optimal_price(self, config):
"""
적정 판매가를 계산하는 메서드입니다.
Parameters:
- config (dict): 가격 정보와 비율을 포함하는 딕셔너리.
Returns:
- optimal_price (float): 계산된 적정 판매가
"""
try:
sold_price = config.get('sold_price') # 팔린가격 없으면 무시
cost_price2X = config['cost_price2X'] * 2 # 원가의 2배
calculated_price = config['calculated_price'] # 원가기준 계산된 가격
lower_bound = config['lower_bound'] # 상한선 비율 (기본값 : 원가2배의 15%)
upper_bound = config['upper_bound'] # 하한선 비율 (기본값 : 원가2배의 -15%)
ratios = config['ratios'] # 계산비율 (기본값 : 팔린가격 50%, 원가2배가격 30%, 계산된가격 20%)
# 비율 기반 계산
weighted_sum = 0
total_ratio = 0
# if sold_price and sold_price > cost_price2X:
if sold_price is not None:
weighted_sum += sold_price * ratios.get('sold_price', 0)
total_ratio += ratios.get('sold_price', 0)
if cost_price2X > 0:
weighted_sum += cost_price2X * ratios.get('cost_price2X', 0) # 원가의 2배
total_ratio += ratios.get('cost_price2X', 0)
if calculated_price > 0:
weighted_sum += calculated_price * ratios.get('calculated_price', 0)
total_ratio += ratios.get('calculated_price', 0)
if total_ratio == 0:
raise ValueError("유효한 가격 정보가 없습니다.")
# 비율에 따른 평균 가격 계산
optimal_price = weighted_sum / total_ratio
optimal_price = self.round_to_UP(optimal_price)
# 상하한 계산
lower_limit = cost_price2X * lower_bound
lower_limit = self.round_to_UP(lower_limit)
upper_limit = cost_price2X * upper_bound
upper_limit = self.round_to_UP(upper_limit)
self.logger.debug(f"비율 기반 계산된 가격: {optimal_price}, 상한: {upper_limit}, 하한: {lower_limit}")
# 상하한 범위 내의 값으로 조정
if optimal_price < lower_limit:
self.logger.debug(f"가격이 하한을 밑돌아서 하한으로 조정됨: {lower_limit}")
return lower_limit
elif optimal_price > upper_limit:
self.logger.debug(f"가격이 상한을 넘어서 상한으로 조정됨: {upper_limit}")
return upper_limit
else:
return optimal_price
except Exception as e:
self.logger.error(f"적정 판매가 계산 실패: {e}", exc_info=True)
return 0 # 예외 발생 시 기본값 0 반환
def calculate_additional_margin_with_extended_thresholds(self, upper_average_price, config, max_threshold=5000000):
"""
점진적으로 증가하는 더하기 마진을 계산하는 메서드입니다.
Parameters:
- upper_average_price (int): 상품의 산술평균과 최대값의 평균 가격.
- config (dict): 구간과 추가 마진 정보를 포함하는 딕셔너리.
- max_threshold (int): 추가 구간을 자동 확장할 최대 가격 (기본값: 5000000).
Returns:
- total_margin (int): 계산된 더하기 마진.
"""
try:
thresholds = config.get('thresholds', [])
additional_margins = config.get('additional_margins', [])
if not thresholds or not additional_margins:
raise ValueError("기준 구간과 추가 마진 정보는 비워둘 수 없습니다.")
self.logger.debug(f"기존 구간: {thresholds}")
self.logger.debug(f"기존 추가 마진: {additional_margins}")
# 주어진 average_price 까지만 확장하도록 변경
while thresholds[-1] < max_threshold and thresholds[-1] < upper_average_price:
last_threshold_diff = thresholds[-1] - thresholds[-2]
new_threshold_diff = last_threshold_diff + (last_threshold_diff - (thresholds[-2] - thresholds[-3]) if len(thresholds) > 2 else last_threshold_diff)
last_margin_diff = additional_margins[-1] - additional_margins[-2]
new_margin_diff = last_margin_diff + (last_margin_diff - (additional_margins[-2] - additional_margins[-3]) if len(additional_margins) > 2 else last_margin_diff)
next_threshold = thresholds[-1] + new_threshold_diff
next_additional_margin = additional_margins[-1] + new_margin_diff
thresholds.append(next_threshold)
additional_margins.append(next_additional_margin)
self.logger.debug(f"확장된 '더하기마진' 구간: {next_threshold}")
self.logger.debug(f"확장된 '더하기마진' 의 추가 마진: {next_additional_margin}")
# 해당 구간에 맞는 마진을 계산
for i in range(len(thresholds)):
if upper_average_price <= thresholds[i]:
# 이전 구간과 현재 구간 사이에서 선형 보간법으로 마진 계산
if i == 0:
self.logger.debug(f"적용된 구간: {thresholds[0]}, 적용된 마진: {additional_margins[0]}")
return additional_margins[0]
prev_threshold = thresholds[i - 1]
prev_margin = additional_margins[i - 1]
current_threshold = thresholds[i]
current_margin = additional_margins[i]
# 선형 보간
calculated_margin = prev_margin + (current_margin - prev_margin) * (upper_average_price - prev_threshold) / (current_threshold - prev_threshold)
calculated_margin = self.round_to_UP(calculated_margin)
self.logger.debug(f"적용된 구간: {current_threshold}, 계산된 마진: {calculated_margin}")
return calculated_margin
# 구간을 넘어섰을 경우, 마지막 구간의 마진을 적용
result = self.round_to_UP(additional_margins[-1])
self.logger.debug(f"최대 구간 초과, 적용된 마진: {result}")
return result
except Exception as e:
self.logger.error(f"계산된 값 입력 실패: {e}", exc_info=True)
return 0 # 예외 발생 시 기본값 0 반환
def calc_initial_price(self, upper_avg_cost, margin_config, shipping_config, card = 0.04, min_margin = 0.24):
"""
초기에 모은 상품원가와 기준가격표를 기준으로 카드수수료, 최소마진등을 기반으로 계산 기준가격을 산출하고
이를 기반으로 테이블표에 의한 더하기마진, 해외배송비를 계산하는 함수.
Parameters:
- upper_avg_cost: 수집된 상품원가의 상위평균값
- card: 카드수수료. 기본값은 상품원가의 4%
- min_margin: 카드수수료가 반영된 상품원가에 대한 비율로 최소마진 추가. 기본값은 24%.
Returns:
- result (int): 초기값으로 계산된 초기가격
"""
try:
# 기본원가
initial_cost_price = upper_avg_cost + (upper_avg_cost * card) + (upper_avg_cost * min_margin)
initial_cost_price = self.round_to_UP(initial_cost_price)
self.logger.debug(f"원가에 카드수수료 {card*100}%, 기본마진 {min_margin*100}% 적용된 계산원가: {initial_cost_price}")
initail_shipping_cost = self.calculate_additional_margin_with_extended_thresholds(initial_cost_price, margin_config)
initail_shipping_cost = self.round_to_UP(initail_shipping_cost)
self.logger.debug(f"계산원가 기준 해외배송비: {initail_shipping_cost}")
initail_margin_cost = self.calculate_shipping_cost_with_extended_thresholds(10000, initial_cost_price, shipping_config)
initail_margin_cost = self.round_to_UP(initail_margin_cost)
self.logger.debug(f"계산원가 기준 더하기마진: {initail_margin_cost}")
result = int(initial_cost_price + initail_shipping_cost + initail_margin_cost)
self.logger.debug(f"원가기반 가격: {result}")
return result
except Exception as e:
self.logger.error(f"원가기반 가격 계산 중 중 오류 발생: {e}", exc_info=True)
return 100000 # 오류발생시 기본 가격 100,000 반환
async def get_option_count_from_text(self):
"""
텍스트 기반으로 옵션 수를 계산합니다. 옵션이 없는 경우 단일 상품으로 간주하여 기본값 1 반환합니다.
Parameters:
- driver: WebDriver 인스턴스
Returns:
- total_options (int): 계산된 옵션 , 단일 상품인 경우 1 반환
"""
try:
# 요소를 기다림
option_count_text_element = await self.page.wait_for_selector(self.option_count_text_locator)
# 텍스트에서 숫자만 추출
option_text = await option_count_text_element.text_content()
match = re.search(r'\d+', option_text)
if match:
total_options = int(match.group()) # 숫자 추출
self.logger.debug(f"옵션 수: {total_options}")
return total_options
else:
self.logger.debug("옵션 수를 찾을 수 없음. 단일 상품으로 간주.")
return 1 # 옵션이 없는 경우 단일 상품으로 간주
except TimeoutError:
self.logger.error("옵션 수를 나타내는 텍스트를 찾지 못함. 기본적으로 단일 상품으로 간주.", exc_info=True)
return 1 # 옵션 수를 찾지 못한 경우 단일 상품으로 간주
except Exception as e:
self.logger.error(f"옵션 수 계산 중 오류 발생: {e}", exc_info=True)
return 1 # 예외 발생 시 기본값 1 반환
async def collect_product_costs_and_prices(self):
"""
상품 원가와 판매가를 수집하여 반환합니다. 단위는 위안화
Parameters:
- driver: WebDriver 인스턴스
Returns:
- option_data (list): 옵션의 원가 가격 데이터
- min_cost (int): 최소 원가, 원화, 에러발생시 기본값 20000 반환
- max_cost (int): 최대 원가, 원화, 에러발생시 기본값 20000 반환
- avg_cost (int): 평균 원가, 원화, 에러발생시 기본값 20000 반환
- upper_avg_cost (int): 평균 원가와 최대값 사이의 평균값, 원화, 에러발생시 기본값 20000 반환
"""
try:
# 옵션 개수 가져오기 (두 가지 방법 중 하나 사용)
total_options = await self.get_option_count_from_text()
self.product_costs = [] # 모든 옵션의 상품원가를 저장할 리스트
self.option_data = [] # 각 옵션의 상품원가와 기준판매가를 저장할 리스트
# 옵션 개수만큼 순회하면서 값을 수집
for i in range(1, total_options + 1):
product_cost_locator = self.product_cost_locator_template.format(i=i+1)
standard_selling_price_locator = self.standard_selling_price_locator_template.format(i=i+1)
# 각 선택자를 사용하여 요소 찾기
product_cost_element = await self.page.wait_for_selector(product_cost_locator)
standard_selling_price_element = await self.page.wait_for_selector(standard_selling_price_locator)
cost_value = int(float(await product_cost_element.input_value().replace(",", "")))
price_value = int(await standard_selling_price_element.input_value().replace(",", ""))
self.product_costs.append(cost_value)
self.option_data.append({
"option_number": i,
"cost": cost_value,
"price": price_value
})
# 상품원가를 가격순으로 정렬
self.product_costs.sort()
# 최소값, 최대값, 평균값 계산
min_cost = self.convert_cny_to_krw(min(self.product_costs)) # 최소값 - 위안화>원화로 변환
min_cost = self.round_to_UP(min_cost) # 1000원 절상
max_cost = self.convert_cny_to_krw(max(self.product_costs)) # 최대값 - 위안화>원화로 변환
max_cost = self.round_to_UP(max_cost) # 1000원 절상
avg_cost = self.convert_cny_to_krw(sum(self.product_costs) / len(self.product_costs)) # 평균값 - 위안화>원화로 변환
avg_cost = self.round_to_UP(avg_cost) # 산술평균값 1000원 절상
upper_avg_cost = round(((avg_cost + max_cost) / 2)) # 상위평균값 - 위안화>원화로 변환
upper_avg_cost = self.round_to_UP(upper_avg_cost) # 상위평균값 1000원 절상
self.logger.debug(f"상품원가가 모였습니다.: {self.product_costs}")
self.logger.debug(f"최소원가: {min_cost}, 최대원가: {max_cost}, 평균원가: {avg_cost}, 상위평균원가: {upper_avg_cost}")
return self.option_data, min_cost, max_cost, avg_cost, upper_avg_cost
except Exception as e:
self.logger.error(f"Failed to collect product costs and prices: {e}", exc_info=True)
# 오류 발생 시 기본값을 반환
return self.option_data, 20000, 20000, 20000, 20000
async def get_plusmargin_and_shipping_values(self):
"""
더하기 마진과 해외 배송비 값을 가져오는 메서드입니다.
Parameters:
- driver: WebDriver 인스턴스
Returns:
- (tuple): 더하기 마진(int), 해외 배송비(int)
"""
try:
# 더하기 마진 값 가져오기
margin_element = await self.page.wait_for_selector(self.selectors['plus_margin_locator'])
margin_value = await margin_element.input_value()
margin = int(margin_value.replace(",", "")) if margin_value else 0
self.logger.debug(f"더하기 마진 값: {margin}")
shipping_element = await self.page.wait_for_selector(self.selectors['oversea_shipping_locator'])
shipping_value = await shipping_element.input_value()
shipping_cost = int(shipping_value.replace(",", "")) if shipping_value else 0
self.logger.debug(f"해외 배송비 값: {shipping_cost}")
return margin, shipping_cost
except Exception as e:
self.logger.error(f"해외배송비와 더하기 마진 수집 중 오류 발생: {e}", exc_info=True)
return 5000, 10000
def set_shipping_config(self, min_price, thresholds, increment_unit, additional_costs):
"""
해외배송비 설정을 위한 딕셔너리를 생성합니다.
Parameters:
- min_price (int): 최소 추가 배송비가 적용되는 가격.
- thresholds (list): 기준 구간 리스트.
- increment_unit (int): 초과 단위.
- additional_costs (list): 구간에 대한 추가 비용 리스트.
Returns:
- config_shipping (dict): 해외배송비 설정 딕셔너리.
"""
return {
'min_price_for_extra_shipping': min_price,
'thresholds': thresholds,
'increment_unit': increment_unit,
'additional_costs': additional_costs
}
def set_margin_config(thresholds, additional_margins):
"""
더하기마진 설정을 위한 딕셔너리를 생성합니다.
Parameters:
- thresholds (list): 기준 구간 리스트.
- additional_margins (list): 구간에 대한 추가 마진 리스트.
Returns:
- config_margin (dict): 더하기마진 설정 딕셔너리.
"""
return {
'thresholds': thresholds,
'additional_margins': additional_margins
}
def set_optimal_price_config(sold_price, cost_price2X, calculated_price, lower_bound, upper_bound, ratios):
"""
적정판매가 설정을 위한 딕셔너리를 생성합니다.
Parameters:
- sold_price (optional): 팔린 가격.
- cost_price2X (int): 원가2배
- calculated_price (int): 계산된 판매가.
- lower_bound (float): 하한 비율.
- upper_bound (float): 상한 비율.
- ratios (dict): 가격 요소에 대한 비율.
Returns:
- config_optimal_price (dict): 적정판매가 설정 딕셔너리.
"""
return {
'sold_price': sold_price,
'cost_price2X': cost_price2X,
'calculated_price': calculated_price,
'lower_bound': lower_bound,
'upper_bound': upper_bound,
'ratios': ratios
}
def round_to_UP(number, nearest=1000):
"""
숫자를 주어진 'nearest' 값에 맞춰 올림합니다.
Parameters:
- number (float or int): 올림할 숫자
- nearest (int): 올림할 단위 (기본값 1000)
Returns:
- rounded_number (int): 올림된 숫자
"""
# nearest 값으로 나눈 후 math.ceil을 사용하여 올림
rounded_number = math.ceil(number / nearest) * nearest
return rounded_number
def convert_cny_to_krw(cny, exchange_rate=200):
"""
위안화 금액을 원화로 변환하는 함수
:param yuan: 변환할 위안화 금액
:param exchange_rate: 위안화에서 원화로 변환할 환율 (기본값: 200)
:return: 변환된 원화 금액
"""
krw = cny * exchange_rate
return krw
def calculate_category_extra_shipping(self, category: str, product_price: int) -> int:
"""
카테고리와 상품가를 기반으로 추가 해외배송비를 계산합니다.
Parameters:
- category (str): 카테고리명
- product_price (int): 상품 가격
Returns:
- total_extra_shipping (int): 계산된 추가 해외배송비
"""
total_extra_shipping = 0
# locator_manager에서 카테고리별 해외배송비 설정을 불러옴
category_data = self.locator_manager.get_category_data(category)
if category_data: # 카테고리 설정이 있는지 확인
threshold_index = 1
# threshold와 extra_shipping의 dynamic한 처리
while True:
threshold_key = f'threshold_{threshold_index}'
extra_shipping_key = f'extra_shipping_{threshold_index}'
unit_key = f'unit_{threshold_index}'
# threshold_key가 존재하지 않으면 종료
if threshold_key not in category_data or extra_shipping_key not in category_data or unit_key not in category_data:
break
threshold = int(category_data.get(threshold_key, 0))
extra_shipping = int(category_data.get(extra_shipping_key, 0))
unit = int(category_data.get(unit_key, 0))
if product_price > threshold:
excess_amount = product_price - threshold
increments = excess_amount // unit
total_extra_shipping += increments * extra_shipping
self.logger.debug(f"{category} 카테고리, threshold {threshold}을 초과하여 추가 해외배송비 {increments * extra_shipping} 추가 적용")
threshold_index += 1 # 다음 구간으로 이동
else:
self.logger.debug(f"카테고리 {category}는 추가 해외배송비 규칙이 없습니다. 추가 해외배송비는 0으로 설정됩니다.")
self.logger.debug(f"총 추가 해외배송비: {total_extra_shipping}")
return total_extra_shipping

801
price_ori.py Normal file
View File

@ -0,0 +1,801 @@
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.common.exceptions import TimeoutException
from selenium.webdriver.common.action_chains import ActionChains
from selenium.webdriver.common.keys import Keys
import time, re, math
from decimal import Decimal, ROUND_CEILING
from edit.action_elements import click_element, return_element, click_and_confirm_tab
# from ai.compare import find_most_similar_image_by_one
import logging
# 로거 인스턴스 가져오기
logger = logging.getLogger('default_logger')
def modify_price_page2(driver, product_infos):
"""
가격 수정 페이지에서 가격을 수집하고 계산 업데이트하는 메인 함수.
Parameters:
- driver: WebDriver 인스턴스
- product_infos: 상품 정보 리스트
"""
# 설정 초기화
shipping_config = set_shipping_config(min_price=50000, thresholds=[50000, 100000, 200000], increment_unit=20000, additional_costs=[5000, 7000, 9000])
margin_config = set_margin_config(thresholds=[50000, 70000, 100000, 150000, 200000, 300000, 400000, 500000, 1000000], additional_margins=[5000, 10000, 15000, 25000, 35000, 50000, 70000, 90000, 120000])
optimal_price_config = set_optimal_price_config(sold_price=None, cost_price2X=None, calculated_price=None, lower_bound=0.85, upper_bound=1.15, ratios={'sold_price': 0.5, 'cost_price2X': 0.3, 'calculated_price': 0.2})
try:
# 가격 탭으로 이동
thumb_data_note = "2"
click_and_confirm_tab(driver, thumb_data_note, 10)
logger.debug("가격탭으로 이동 완료")
time.sleep(2) # 페이지 로딩 대기
# 1. 페이지에서 가격 정보 수집
logger.debug("초기 더하기마진과 해외배송비 가격 정보를 수집합니다.")
initial_plusmargin, initial_shpping_price = get_plusmargin_and_shpping_values(driver)
sold_price = 0
# if initial_plusmargin and initial_plusmargin != 10000:
if (initial_plusmargin != 10000 and initial_shpping_price != 0):
sold_price = initial_plusmargin
logger.debug(f"더하기마진과 해외배송비가 기본값이 아니므로 더하기마진값{initial_plusmargin}을 팔린가격{sold_price}으로 간주")
logger.debug("옵션 가격 정보를 수집합니다.")
option_data, min_cost, max_cost, avg_cost, upper_avg_cost = collect_product_costs_and_prices(driver) # 수집된 옵션정보를 반환
if option_data is None:
logger.error("상품 옵션 정보를 수집하지 못했습니다.")
return # 옵션정보가 수집되지 않을 경우 modify_price_page2 메서드를 조기 종료하여 더이상의 작업진행을 멈추어서 안전하게 종료
# 2. 원가 기반가격 계산
calculated_price = calc_initial_price(upper_avg_cost, margin_config, shipping_config, 0.04, 0.24)
# 3. 적정 판매가 산출
logger.debug("적정 판매가를 계산합니다.")
optimal_price_config['sold_price'] = sold_price # 팔린가격 기본값(10000이 아닌 값이 있을 경우 팔린가격으로 간주)
optimal_price_config['cost_price2X'] = upper_avg_cost # 원가2배 가격 : upper_avg_cost를 기준으로 계산.
optimal_price_config['calculated_price'] = calculated_price # 기준판매가를 기준으로 계산된 값
optimal_price = calculate_optimal_price(optimal_price_config)
logger.debug(f"계산된 적정 판매가: {optimal_price}")
# 4. 더하기 마진을 적정 판매가 기준으로 재설정
logger.debug("더하기 마진을 적정 판매가에 맞게 조정합니다.")
additional_margin = calculate_adjusted_margin(optimal_price, option_data, margin_config)
additional_margin = round_to_UP(additional_margin)
logger.debug(f"조정된 더하기 마진: {additional_margin}")
# 5. 해외 배송비 재계산
shipping_base_price = optimal_price - upper_avg_cost - (upper_avg_cost*0.04) - (upper_avg_cost*0.24) - additional_margin
shipping_cost = calculate_shipping_cost_with_extended_thresholds(10000, shipping_base_price, shipping_config)
# shipping_cost = calculate_shipping_cost_with_extended_thresholds(10000, optimal_price, shipping_config)
shipping_cost = round_to_UP(shipping_cost)
logger.debug(f"적정판매가 기준으로 재계산된 해외배송비: {shipping_cost}")
# 5. 계산된 값 입력
logger.debug("계산된 값을 페이지에 입력합니다.")
input_calculated_values(driver, additional_margin, shipping_cost)
# 6. 반품비, 초도배송비, 교환비 계산 및 입력
return_fee, first_delv_fee, exchange_fee = calculate_claim_costs(max_cost)
logger.debug(f"반품비: {return_fee}, 초도배송비: {first_delv_fee}, 교환비: {exchange_fee}")
input_claim_costs(driver, return_fee, first_delv_fee, exchange_fee)
# 7. 저장
save_xpath = "//button[contains(.,'저장하기')]"
click_element(driver, 'XPATH', save_xpath, 10, 'js')
logger.debug("가격 정리 후 저장버튼 클릭 완료")
# 가격 수정 완료 후 complete_tab 호출하여 ESC 키 전송
# complete_tab(driver)
# logger.debug("가격 정리 후 ESC 전송")
except Exception as e:
logger.error(f"가격 수정 중 오류 발생: {e}", exc_info=True)
def calc_initial_price(upper_avg_cost, margin_config, shipping_config, card = 0.04, min_margin = 0.24):
"""
초기에 모은 상품원가와 기준가격표를 기준으로 카드수수료, 최소마진등을 기반으로 계산 기준가격을 산출하고
이를 기반으로 테이블표에 의한 더하기마진, 해외배송비를 계산하는 함수.
Parameters:
- upper_avg_cost: 수집된 상품원가의 상위평균값
- card: 카드수수료. 기본값은 상품원가의 4%
- min_margin: 카드수수료가 반영된 상품원가에 대한 비율로 최소마진 추가. 기본값은 24%.
Returns:
- result (int): 초기값으로 계산된 초기가격
"""
try:
# 기본원가
initial_cost_price = upper_avg_cost + (upper_avg_cost * card) + (upper_avg_cost * min_margin)
initial_cost_price = round_to_UP(initial_cost_price)
logger.debug(f"원가에 카드수수료 {card*100}%, 기본마진 {min_margin*100}% 적용된 계산원가: {initial_cost_price}")
initail_shipping_cost = calculate_additional_margin_with_extended_thresholds(initial_cost_price, margin_config)
initail_shipping_cost = round_to_UP(initail_shipping_cost)
logger.debug(f"계산원가 기준 해외배송비: {initail_shipping_cost}")
initail_margin_cost = calculate_shipping_cost_with_extended_thresholds(10000, initial_cost_price, shipping_config)
initail_margin_cost = round_to_UP(initail_margin_cost)
logger.debug(f"계산원가 기준 더하기마진: {initail_margin_cost}")
result = int(initial_cost_price + initail_shipping_cost + initail_margin_cost)
logger.debug(f"원가기반 가격: {result}")
return result
except Exception as e:
logger.error(f"원가기반 가격 계산 중 중 오류 발생: {e}", exc_info=True)
return 100000 # 오류발생시 기본 가격 100,000 반환
def calculate_adjusted_margin(optimal_price, option_data, margin_config):
"""
적정판매가와 기준판매가를 비교하여 더하기 마진을 조정하는 메서드입니다.
Parameters:
- optimal_price (int): 계산된 적정 판매가.
- option_data (list): 옵션의 데이터.
- margin_config (dict): 더하기 마진 설정 딕셔너리.
Returns:
- adjusted_margin (int): 적정 판매가에 맞게 조정된 더하기 마진.
"""
try:
total_option_price = math.ceil(sum([option['price'] for option in option_data]) / len(option_data))
logger.debug(f"총 옵션 기준 판매가 평균: {total_option_price}")
# 기준판매가와 적정판매가의 차이를 바탕으로 마진을 조정
price_difference = optimal_price - total_option_price
logger.debug(f"적정 판매가와 기준 판매가 차이: {price_difference}")
# 마진 설정: 차이에 따른 더하기 마진 재조정
adjusted_margin = calculate_additional_margin_with_extended_thresholds(optimal_price, margin_config)
# 만약 차이가 크다면, 추가로 마진을 더 조정할 수 있음
if price_difference > 0:
adjusted_margin += price_difference * 0.1 # 차이의 10%를 추가 마진에 반영
return adjusted_margin
except Exception as e:
logger.error(f"더하기 마진 조정 중 오류 발생: {e}", exc_info=True)
return 0 # 기본값 반환
def calculate_claim_costs(max_cost):
"""
반품비, 초도배송비, 교환비를 계산합니다.
Parameters:
- max_cost (int): 상품의 최대 원가
Returns:
- return_fee (int): 반품비
- first_delv_fee (int): 초도배송비
- exchange_fee (int): 교환비
"""
try:
max_return_fee = 199000
max_exchange_fee = 499000
return_fee = min(max_return_fee, max_cost)
return_fee = round_to_UP(return_fee)
first_delv_fee = return_fee # 초도배송비는 반품비와 동일
exchange_fee = min(max_exchange_fee, return_fee * 2)
exchange_fee = round_to_UP(exchange_fee)
logger.debug(f"계산된 반품비: {return_fee}, 초도배송비: {first_delv_fee}, 교환비: {exchange_fee}")
return return_fee, first_delv_fee, exchange_fee
except Exception as e:
logger.error(f"Claim cost 계산 중 오류 발생: {e}", exc_info=True)
return 199000, 199000, 499000
def clear_and_send_keys(element, value):
element.send_keys(Keys.CONTROL + "a") # 모든 텍스트 선택
element.send_keys(Keys.DELETE) # 선택된 텍스트 삭제
element.send_keys(str(value)) # 새 값 입력
def clear_input_field(driver, element):
driver.execute_script("arguments[0].value = '';", element)
def input_claim_costs(driver, return_fee, first_delv_fee, exchange_fee):
"""
계산된 반품비, 초도배송비, 교환비를 입력합니다.
Parameters:
- driver: WebDriver 인스턴스
- return_fee (int): 반품비
- first_delv_fee (int): 초도배송비
- exchange_fee (int): 교환비
"""
try:
# 반품비 수정
click_element(driver, 'XPATH', "//*[@id='productMainContentContainerId']/div/div[1]/div/div/div[4]/div/div[1]/div[3]/div/div/div/div[1]/div[2]/input", 10, 'ac')
element = return_element(driver, 'XPATH', "//*[@id='productMainContentContainerId']/div/div[1]/div/div/div[4]/div/div[1]/div[3]/div/div/div/div[1]/div[2]/input", 10)
if element:
clear_input_field(driver, element) # JavaScript를 사용해 필드 비우기
return_fee = round_to_UP(return_fee)
element.send_keys(return_fee)
logger.debug(f"반품비 수정 완료 : {return_fee}")
else:
logger.error(f"반품비 입력 중 오류 발생: 요소를 찾을 수 없음")
# 초도배송비 수정
click_element(driver, 'XPATH', "//*[@id='productMainContentContainerId']/div/div[1]/div/div/div[4]/div/div[1]/div[4]/div/div[2]/div/div[1]/div[2]/input", 10, 'ac')
element = return_element(driver, 'XPATH', "//*[@id='productMainContentContainerId']/div/div[1]/div/div/div[4]/div/div[1]/div[4]/div/div[2]/div/div[1]/div[2]/input", 10)
if element:
# clear_and_send_keys(element, first_delv_fee) # SendKey를 사용해 필드 비우기
clear_input_field(driver, element) # JavaScript를 사용해 필드 비우기
first_delv_fee = round_to_UP(first_delv_fee)
element.send_keys(first_delv_fee)
logger.debug(f"초도배송비 수정 완료 : {first_delv_fee}")
else:
logger.error(f"초도배송비 입력 중 오류 발생: 요소를 찾을 수 없음")
# 교환비 수정
click_element(driver, 'XPATH', "//*[@id='productMainContentContainerId']/div/div[1]/div/div/div[4]/div/div[1]/div[5]/div/div/div/div[1]/div[2]/input", 10, 'ac')
element = return_element(driver, 'XPATH', "//*[@id='productMainContentContainerId']/div/div[1]/div/div/div[4]/div/div[1]/div[5]/div/div/div/div[1]/div[2]/input", 10)
if element:
clear_input_field(driver, element) # JavaScript를 사용해 필드 비우기
exchange_fee = round_to_UP(exchange_fee)
element.send_keys(exchange_fee)
logger.debug(f"교환비 수정 완료 : {exchange_fee}")
else:
logger.error(f"교환비 입력 중 오류 발생: 요소를 찾을 수 없음")
except Exception as e:
logger.error(f"Claim cost 수정 중 오류 발생: {e}", exc_info=True)
def get_option_count_from_input_boxes(driver):
"""
입력 박스를 기반으로 옵션 수를 계산합니다.
Parameters:
- driver: WebDriver 인스턴스
Returns:
- total_options (int): 계산된 옵션
"""
try:
input_boxes = driver.find_elements(By.CSS_SELECTOR, "div#productMainContentContainerId div.gbcihF.sc-iUwfNp input.ant-input-number-input")
total_options = len(input_boxes) // 3 # 인풋 박스 개수를 3으로 나누어 옵션 개수를 계산
logger.debug(f"Total options calculated from input boxes: {total_options}")
return total_options
except Exception as e:
logger.error(f"Failed to calculate option count from input boxes: {e}", exc_info=True)
return 1 # 오류 발생 시 기본값 1 반환
def get_option_count_from_text(driver):
"""
텍스트 기반으로 옵션 수를 계산합니다. 옵션이 없는 경우 단일 상품으로 간주하여 기본값 1 반환합니다.
Parameters:
- driver: WebDriver 인스턴스
Returns:
- total_options (int): 계산된 옵션 , 단일 상품인 경우 1 반환
"""
try:
# 옵션 수를 나타내는 텍스트의 CSS 선택자
# option_text_css = "div#productMainContentContainerId div.gbcihF.sc-iUwfNp div.ant-table-header thead.ant-table-thead div.ant-flex.ant-flex-align-center.css-1li46mu[style='gap: 8px;'] span"
#productMainContentContainerId > div > div:nth-child(2) > div > div > div.sc-eiQriw.kvXidT > div.ant-table-wrapper.css-1li46mu > div > div > div > div > div.ant-table-header.ant-table-sticky-holder > table > thead > tr > th:nth-child(2) > div > span
option_text_css = "div#productMainContentContainerId th:nth-child(2) > div > span"
# 요소를 기다림
option_text_element = WebDriverWait(driver, 10).until(
EC.presence_of_element_located((By.CSS_SELECTOR, option_text_css))
)
# 텍스트에서 숫자만 추출
option_text = option_text_element.text
match = re.search(r'\d+', option_text)
if match:
total_options = int(match.group()) # 숫자 추출
logger.debug(f"옵션 수: {total_options}")
return total_options
else:
logger.debug("옵션 수를 찾을 수 없음. 단일 상품으로 간주.")
return 1 # 옵션이 없는 경우 단일 상품으로 간주
except TimeoutException:
logger.error("옵션 수를 나타내는 텍스트를 찾지 못함. 기본적으로 단일 상품으로 간주.")
return 1 # 옵션 수를 찾지 못한 경우 단일 상품으로 간주
except Exception as e:
logger.error(f"옵션 수 계산 중 오류 발생: {e}", exc_info=True)
return 1 # 예외 발생 시 기본값 1 반환
def collect_product_costs_and_prices(driver):
"""
상품 원가와 판매가를 수집하여 반환합니다. 단위는 위안화
Parameters:
- driver: WebDriver 인스턴스
Returns:
- option_data (list): 옵션의 원가 가격 데이터
- min_cost (int): 최소 원가, 원화, 에러발생시 기본값 20000 반환
- max_cost (int): 최대 원가, 원화, 에러발생시 기본값 20000 반환
- avg_cost (int): 평균 원가, 원화, 에러발생시 기본값 20000 반환
- upper_avg_cost (int): 평균 원가와 최대값 사이의 평균값, 원화, 에러발생시 기본값 20000 반환
"""
try:
# 옵션 개수 가져오기 (두 가지 방법 중 하나 사용)
total_options = get_option_count_from_text(driver)
product_costs = [] # 모든 옵션의 상품원가를 저장할 리스트
option_data = [] # 각 옵션의 상품원가와 기준판매가를 저장할 리스트
# 옵션 개수만큼 순회하면서 값을 수집
for i in range(1, total_options + 1):
# 상품원가 가져오기 (1번째 인풋박스)
cost_xpath = f"//*[@id='productMainContentContainerId']/div/div[2]/div/div/div[5]/div[1]/div/div/div/div/div[2]/table/tbody/tr[{i+1}]/td[3]/div/div/div/div[2]/input"
cost_element = return_element(driver, 'XPATH', cost_xpath, 10)
try:
cost_value = int(float(cost_element.get_attribute('value').replace(",", ""))) # 소수점을 처리하여 정수로 변환
except ValueError:
cost_value = 100 # 기본값 설정
logger.error(f"Invalid cost value for option {i}: '{cost_element.get_attribute('value')}', setting to 0.")
product_costs.append(cost_value)
# 기준판매가 가져오기 (2번째 인풋박스)
price_xpath = f"//*[@id='productMainContentContainerId']/div/div[2]/div/div/div[5]/div[1]/div/div/div/div/div[2]/table/tbody/tr[{i+1}]/td[4]/div/div/div[1]/div/div[2]/input"
price_element = return_element(driver, 'XPATH', price_xpath, 10)
try:
price_value = int(price_element.get_attribute('value').replace(",", ""))
except ValueError:
price_value = 30000 # 기본값 설정
logger.error(f"Invalid price value for option {i}: '{price_element.get_attribute('value')}', setting to 0.")
# 옵션 데이터를 딕셔너리에 저장
option_data.append({
"option_number": i,
"cost": cost_value,
"price": price_value
})
# 상품원가를 가격순으로 정렬
product_costs.sort()
# 최소값, 최대값, 평균값 계산
min_cost = convert_cny_to_krw(min(product_costs)) # 최소값 - 위안화>원화로 변환
min_cost = round_to_UP(min_cost) # 1000원 절상
max_cost = convert_cny_to_krw(max(product_costs)) # 최대값 - 위안화>원화로 변환
max_cost = round_to_UP(max_cost) # 1000원 절상
avg_cost = convert_cny_to_krw(sum(product_costs) / len(product_costs)) # 평균값 - 위안화>원화로 변환
avg_cost = round_to_UP(avg_cost) # 산술평균값 1000원 절상
upper_avg_cost = round(((avg_cost + max_cost) / 2)) # 상위평균값 - 위안화>원화로 변환
upper_avg_cost = round_to_UP(upper_avg_cost) # 상위평균값 1000원 절상
logger.debug(f"상품원가가 모였습니다.: {product_costs}")
logger.debug(f"최소원가: {min_cost}, 최대원가: {max_cost}, 평균원가: {avg_cost}, 상위평균원가: {upper_avg_cost}")
return option_data, min_cost, max_cost, avg_cost, upper_avg_cost
except Exception as e:
logger.error(f"Failed to collect product costs and prices: {e}", exc_info=True)
# 오류 발생 시 기본값을 반환
return option_data, 20000, 20000, 20000, 20000
def input_calculated_values(driver, margin, shipping_cost):
"""
계산된 마진, 배송비, 교환비를 옵션에 입력합니다.
Parameters:
- driver: WebDriver 인스턴스
- margin (int): 더하기 마진
- shipping_cost (int): 해외배송비
"""
try:
# 1000원 단위 절상
margin = round_to_UP(margin)
shipping_cost = round_to_UP(shipping_cost)
# 더하기 마진 입력 (4번째 인풋박스)
margin_xpath = f"//*[@id='productMainContentContainerId']/div/div[1]/div/div/div[2]/div/div[1]/div[8]/div/div/div[3]/div/div/div/div[1]/div[2]/input"
margin_element = WebDriverWait(driver, 10).until(
EC.element_to_be_clickable((By.XPATH, margin_xpath))
)
# clear_input_field(driver, margin_element) # JavaScript를 사용해 필드 비우기
# margin_element.send_keys(str(margin))
clear_and_send_keys(margin_element, margin)
logger.debug(f"더하기 마진 입력 완료: {margin}")
# 해외 배송비 입력 (5번째 인풋박스)
shipping_xpath = f"//*[@id='productMainContentContainerId']/div/div[1]/div/div/div[2]/div/div[1]/div[10]/div/div/div/div[1]/div[2]/input"
shipping_element = WebDriverWait(driver, 10).until(
EC.element_to_be_clickable((By.XPATH, shipping_xpath))
)
# clear_input_field(driver, shipping_element) # JavaScript를 사용해 필드 비우기
# shipping_element.send_keys(str(shipping_cost))
clear_and_send_keys(shipping_element, shipping_cost)
logger.debug(f"해외 배송비 입력 완료: {shipping_cost}")
except Exception as e:
logger.error(f"Failed to input calculated values: {e}", exc_info=True)
def get_plusmargin_and_shpping_values(driver):
"""
더하기 마진과 해외 배송비 값을 가져오는 메서드입니다.
Parameters:
- driver: WebDriver 인스턴스
Returns:
- (tuple): 더하기 마진(int), 해외 배송비(int)
"""
try:
# 더하기 마진 값 가져오기
margin_xpath = f"//*[@id='productMainContentContainerId']/div/div[1]/div/div/div[2]/div/div[1]/div[8]/div/div/div[3]/div/div/div/div[1]/div[2]/input"
margin_element = WebDriverWait(driver, 10).until(
EC.presence_of_element_located((By.XPATH, margin_xpath))
)
margin_value = margin_element.get_attribute("value")
# 쉼표 제거 후 정수로 변환
margin = int(margin_value.replace(",", "")) if margin_value else 0
logger.debug(f"더하기 마진 값 가져오기 완료: {margin}")
# 해외 배송비 값 가져오기
shipping_xpath = f"//*[@id='productMainContentContainerId']/div/div[1]/div/div/div[2]/div/div[1]/div[10]/div/div/div/div[1]/div[2]/input"
shipping_element = WebDriverWait(driver, 10).until(
EC.presence_of_element_located((By.XPATH, shipping_xpath))
)
shipping_value = shipping_element.get_attribute("value")
# 쉼표 제거 후 정수로 변환
shipping_cost = int(shipping_value.replace(",", "")) if shipping_value else 0
logger.debug(f"해외 배송비 값 가져오기 완료: {shipping_cost}")
return margin, shipping_cost
except Exception as e:
logger.error(f"Failed to get calculated values: {e}", exc_info=True)
return 5000, 10000 # 오류발생시 기본값 5000, 10000 반환
def calculate_additional_margin_with_extended_thresholds(upper_average_price, config, max_threshold=5000000):
"""
점진적으로 증가하는 더하기 마진을 계산하는 메서드입니다.
Parameters:
- upper_average_price (int): 상품의 산술평균과 최대값의 평균 가격.
- config (dict): 구간과 추가 마진 정보를 포함하는 딕셔너리.
- max_threshold (int): 추가 구간을 자동 확장할 최대 가격 (기본값: 5000000).
Returns:
- total_margin (int): 계산된 더하기 마진.
"""
try:
thresholds = config.get('thresholds', [])
additional_margins = config.get('additional_margins', [])
if not thresholds or not additional_margins:
raise ValueError("기준 구간과 추가 마진 정보는 비워둘 수 없습니다.")
logger.debug(f"기존 구간: {thresholds}")
logger.debug(f"기존 추가 마진: {additional_margins}")
# 주어진 average_price 까지만 확장하도록 변경
while thresholds[-1] < max_threshold and thresholds[-1] < upper_average_price:
last_threshold_diff = thresholds[-1] - thresholds[-2]
new_threshold_diff = last_threshold_diff + (last_threshold_diff - (thresholds[-2] - thresholds[-3]) if len(thresholds) > 2 else last_threshold_diff)
last_margin_diff = additional_margins[-1] - additional_margins[-2]
new_margin_diff = last_margin_diff + (last_margin_diff - (additional_margins[-2] - additional_margins[-3]) if len(additional_margins) > 2 else last_margin_diff)
next_threshold = thresholds[-1] + new_threshold_diff
next_additional_margin = additional_margins[-1] + new_margin_diff
thresholds.append(next_threshold)
additional_margins.append(next_additional_margin)
logger.debug(f"확장된 '더하기마진' 구간: {next_threshold}")
logger.debug(f"확장된 '더하기마진' 의 추가 마진: {next_additional_margin}")
# 해당 구간에 맞는 마진을 계산
for i in range(len(thresholds)):
if upper_average_price <= thresholds[i]:
# 이전 구간과 현재 구간 사이에서 선형 보간법으로 마진 계산
if i == 0:
logger.debug(f"적용된 구간: {thresholds[0]}, 적용된 마진: {additional_margins[0]}")
return additional_margins[0]
prev_threshold = thresholds[i - 1]
prev_margin = additional_margins[i - 1]
current_threshold = thresholds[i]
current_margin = additional_margins[i]
# 선형 보간
calculated_margin = prev_margin + (current_margin - prev_margin) * (upper_average_price - prev_threshold) / (current_threshold - prev_threshold)
calculated_margin = round_to_UP(calculated_margin)
logger.debug(f"적용된 구간: {current_threshold}, 계산된 마진: {calculated_margin}")
return calculated_margin
# 구간을 넘어섰을 경우, 마지막 구간의 마진을 적용
result = round_to_UP(additional_margins[-1])
logger.debug(f"최대 구간 초과, 적용된 마진: {result}")
return result
except Exception as e:
logger.error(f"계산된 값 입력 실패: {e}", exc_info=True)
return 0 # 예외 발생 시 기본값 0 반환
def calculate_shipping_cost_with_extended_thresholds(base_cost, product_price, config, max_threshold=5000000):
"""
점진적으로 증가하는 해외배송비를 계산하는 메서드입니다.
Parameters:
- base_cost (int): 기본 해외배송비.
- product_price (int): 상품의 가격.
- config (dict): 구간과 추가 배송비 정보를 포함하는 딕셔너리.
- max_threshold (int): 추가 구간을 자동 확장할 최대 가격 (기본값: 5000000).
Returns:
- total_shipping_cost (int): 계산된 해외배송비.
"""
try:
total_shipping_cost = base_cost
min_price_for_extra_shipping = config.get('min_price_for_extra_shipping', 0)
thresholds = config.get('thresholds', [])
increment_unit = config.get('increment_unit', 0)
additional_costs = config.get('additional_costs', [])
if not thresholds or not additional_costs:
raise ValueError("기준 구간과 추가 비용 정보는 비워둘 수 없습니다.")
logger.debug(f"기존 구간: {thresholds}")
logger.debug(f"기존 추가 비용: {additional_costs}")
# 주어진 product_price 까지만 확장하도록 변경
while thresholds[-1] < max_threshold and thresholds[-1] < product_price:
last_threshold_diff = thresholds[-1] - thresholds[-2]
new_threshold_diff = last_threshold_diff + (last_threshold_diff - (thresholds[-2] - thresholds[-3]) if len(thresholds) > 2 else last_threshold_diff)
last_cost_diff = additional_costs[-1] - additional_costs[-2]
new_cost_diff = last_cost_diff + (last_cost_diff - (additional_costs[-2] - additional_costs[-3]) if len(additional_costs) > 2 else last_cost_diff)
next_threshold = thresholds[-1] + new_threshold_diff
next_additional_cost = additional_costs[-1] + new_cost_diff
thresholds.append(next_threshold)
additional_costs.append(next_additional_cost)
logger.debug(f"확장된 '해외배송비' 구간: {next_threshold}")
logger.debug(f"확장된 '해외배송비' 추가 비용: {next_additional_cost}")
# 상품 가격이 최소 추가 배송비 적용 금액 이상일 때만 추가 비용을 계산
if product_price > min_price_for_extra_shipping:
remaining_price = product_price - min_price_for_extra_shipping
for i in range(len(thresholds)):
if remaining_price > 0:
if i < len(thresholds) - 1:
next_threshold = thresholds[i + 1]
if remaining_price <= next_threshold - min_price_for_extra_shipping:
increments = remaining_price // increment_unit
total_shipping_cost += increments * additional_costs[i]
logger.debug(f"적용된 구간: {next_threshold}, 계산된 구간추가배송비: {increments * additional_costs[i]}")
remaining_price = 0
else:
increments = (next_threshold - min_price_for_extra_shipping) // increment_unit
total_shipping_cost += increments * additional_costs[i]
logger.debug(f"적용된 구간: {next_threshold}, 계산된 구간추가배송비: {increments * additional_costs[i]}")
remaining_price -= next_threshold - min_price_for_extra_shipping
min_price_for_extra_shipping = next_threshold
else:
increments = remaining_price // increment_unit
total_shipping_cost += increments * additional_costs[i]
logger.debug(f"최대 구간 적용, 계산된 구간추가배송비: {increments * additional_costs[i]}")
remaining_price = 0
result = round_to_UP(total_shipping_cost)
logger.debug(f"최종 계산된 배송비: {result}")
return result
except ValueError as ve:
logger.error(f"기본 구성 오류: {ve}", exc_info=True)
return base_cost # 예외 발생 시 기본 배송비만 반환
except Exception as e:
logger.error(f"계산된 값 입력 실패: {e}", exc_info=True)
return max(base_cost, 10000) # 예외 발생 시 최소 기본 배송비 반환
def calculate_optimal_price(config):
"""
적정 판매가를 계산하는 메서드입니다.
Parameters:
- config (dict): 가격 정보와 비율을 포함하는 딕셔너리.
Returns:
- optimal_price (float): 계산된 적정 판매가
"""
try:
sold_price = config.get('sold_price') # 팔린가격 없으면 무시
cost_price2X = config['cost_price2X'] * 2 # 원가의 2배
calculated_price = config['calculated_price'] # 원가기준 계산된 가격
lower_bound = config['lower_bound'] # 상한선 비율 (기본값 : 원가2배의 15%)
upper_bound = config['upper_bound'] # 하한선 비율 (기본값 : 원가2배의 -15%)
ratios = config['ratios'] # 계산비율 (기본값 : 팔린가격 50%, 원가2배가격 30%, 계산된가격 20%)
# 비율 기반 계산
weighted_sum = 0
total_ratio = 0
# if sold_price and sold_price > cost_price2X:
if sold_price is not None:
weighted_sum += sold_price * ratios.get('sold_price', 0)
total_ratio += ratios.get('sold_price', 0)
if cost_price2X > 0:
weighted_sum += cost_price2X * ratios.get('cost_price2X', 0) # 원가의 2배
total_ratio += ratios.get('cost_price2X', 0)
if calculated_price > 0:
weighted_sum += calculated_price * ratios.get('calculated_price', 0)
total_ratio += ratios.get('calculated_price', 0)
if total_ratio == 0:
raise ValueError("유효한 가격 정보가 없습니다.")
# 비율에 따른 평균 가격 계산
optimal_price = weighted_sum / total_ratio
optimal_price = round_to_UP(optimal_price)
# 상하한 계산
lower_limit = cost_price2X * lower_bound
lower_limit = round_to_UP(lower_limit)
upper_limit = cost_price2X * upper_bound
upper_limit = round_to_UP(upper_limit)
logger.debug(f"비율 기반 계산된 가격: {optimal_price}, 상한: {upper_limit}, 하한: {lower_limit}")
# 상하한 범위 내의 값으로 조정
if optimal_price < lower_limit:
logger.debug(f"가격이 하한을 밑돌아서 하한으로 조정됨: {lower_limit}")
return lower_limit
elif optimal_price > upper_limit:
logger.debug(f"가격이 상한을 넘어서 상한으로 조정됨: {upper_limit}")
return upper_limit
else:
return optimal_price
except Exception as e:
logger.error(f"적정 판매가 계산 실패: {e}", exc_info=True)
return 0 # 예외 발생 시 기본값 0 반환
def set_shipping_config(min_price, thresholds, increment_unit, additional_costs):
"""
해외배송비 설정을 위한 딕셔너리를 생성합니다.
Parameters:
- min_price (int): 최소 추가 배송비가 적용되는 가격.
- thresholds (list): 기준 구간 리스트.
- increment_unit (int): 초과 단위.
- additional_costs (list): 구간에 대한 추가 비용 리스트.
Returns:
- config_shipping (dict): 해외배송비 설정 딕셔너리.
"""
return {
'min_price_for_extra_shipping': min_price,
'thresholds': thresholds,
'increment_unit': increment_unit,
'additional_costs': additional_costs
}
def set_margin_config(thresholds, additional_margins):
"""
더하기마진 설정을 위한 딕셔너리를 생성합니다.
Parameters:
- thresholds (list): 기준 구간 리스트.
- additional_margins (list): 구간에 대한 추가 마진 리스트.
Returns:
- config_margin (dict): 더하기마진 설정 딕셔너리.
"""
return {
'thresholds': thresholds,
'additional_margins': additional_margins
}
def set_optimal_price_config(sold_price, cost_price2X, calculated_price, lower_bound, upper_bound, ratios):
"""
적정판매가 설정을 위한 딕셔너리를 생성합니다.
Parameters:
- sold_price (optional): 팔린 가격.
- cost_price2X (int): 원가2배
- calculated_price (int): 계산된 판매가.
- lower_bound (float): 하한 비율.
- upper_bound (float): 상한 비율.
- ratios (dict): 가격 요소에 대한 비율.
Returns:
- config_optimal_price (dict): 적정판매가 설정 딕셔너리.
"""
return {
'sold_price': sold_price,
'cost_price2X': cost_price2X,
'calculated_price': calculated_price,
'lower_bound': lower_bound,
'upper_bound': upper_bound,
'ratios': ratios
}
def display_config(config, config_name):
"""
주어진 설정 딕셔너리를 출력합니다.
Parameters:
- config (dict): 설정 딕셔너리.
- config_name (str): 딕셔너리 이름.
"""
logger.debug(f"=== {config_name} 설정 ===")
for key, value in config.items():
logger.debug(f"{key}: {value}")
logger.debug("===================")
def complete_tab(driver):
"""
ESC 키를 전송하여 현재 탭을 닫는 메서드.
Parameters:
- driver: WebDriver 인스턴스
"""
try:
ActionChains(driver).send_keys(Keys.ESCAPE).perform()
logger.debug("ESC 키 전송 완료: 탭이 닫혔습니다.")
except Exception as e:
logger.error(f"ESC 키 전송 실패: {e}", exc_info=True)
def round_to_UP(number, nearest=1000):
"""
숫자를 주어진 'nearest' 값에 맞춰 올림합니다.
Parameters:
- number (float or int): 올림할 숫자
- nearest (int): 올림할 단위 (기본값 1000)
Returns:
- rounded_number (int): 올림된 숫자
"""
# nearest 값으로 나눈 후 math.ceil을 사용하여 올림
rounded_number = math.ceil(number / nearest) * nearest
return rounded_number
# def round_to_UP(number, nearest='100'):
# decimal_number = Decimal(number)
# rounded_number = int(decimal_number.quantize(Decimal(nearest), rounding=ROUND_CEILING))
# return rounded_number
def convert_cny_to_krw(cny, exchange_rate=200):
"""
위안화 금액을 원화로 변환하는 함수
:param yuan: 변환할 위안화 금액
:param exchange_rate: 위안화에서 원화로 변환할 환율 (기본값: 200)
:return: 변환된 원화 금액
"""
krw = cny * exchange_rate
return krw

View File

@ -6,9 +6,10 @@ from pyvda import VirtualDesktop, get_virtual_desktops
import subprocess import subprocess
import asyncio import asyncio
import KO_EN import KO_EN
import pyperclip # 클립보드 데이터를 확인하기 위한 라이브러리
class WhaleTranslator: class WhaleTranslator:
def __init__(self, app, logger, secret_mode=True, vd_mode=False): def __init__(self, app, logger, secret_mode=True, vd_mode=False, max_failures=5):
self.app = app self.app = app
self.logger = logger self.logger = logger
self.vd_mode = vd_mode self.vd_mode = vd_mode
@ -16,6 +17,10 @@ class WhaleTranslator:
self.whale_pid = None self.whale_pid = None
isSecret = secret_mode isSecret = secret_mode
self.translation_success_flag = False # 번역 성공 플래그
self.failure_count = 0 # 실패 횟수
self.max_failures = max_failures # 최대 실패 횟수
if isSecret: if isSecret:
self.whale_window_name = "새 시크릿 탭 - Whale" self.whale_window_name = "새 시크릿 탭 - Whale"
else: else:
@ -69,6 +74,32 @@ class WhaleTranslator:
if self.vd_mode: if self.vd_mode:
self.return_to_virtual_desktop_1() # 가상 데스크탑 1로 복귀 self.return_to_virtual_desktop_1() # 가상 데스크탑 1로 복귀
def reset_failures(self):
"""실패 횟수를 초기화"""
self.failure_count = 0
self.logger.debug("실패 횟수가 초기화되었습니다.")
def handle_translation_failure(self):
"""번역 실패 시 처리"""
self.failure_count += 1
self.logger.error(f"번역 실패! 실패 횟수: {self.failure_count}/{self.max_failures}")
if self.failure_count >= self.max_failures:
self.logger.error("최대 실패 횟수에 도달했습니다. 웨일 브라우저를 재시작합니다.")
self.close_whale_window_if_exists()
time.sleep(2) # 재시작 전에 짧은 대기
asyncio.run(self.start_whale_browser()) # 브라우저 재시작
self.reset_failures() # 실패 횟수 초기화
def is_image_in_clipboard(self):
"""클립보드에 이미지 데이터 또는 base64로 인코딩된 이미지 데이터가 있는지 확인"""
clipboard_content = pyperclip.paste()
if clipboard_content.startswith("data:image") or isinstance(clipboard_content, bytes):
self.logger.debug("클립보드에 이미지 데이터가 확인되었습니다.")
return True
else:
self.logger.debug("클립보드에 이미지 데이터가 없습니다.")
return False
def find_whale_window(self): def find_whale_window(self):
"""웨일 창 핸들을 찾는 메서드""" """웨일 창 핸들을 찾는 메서드"""
@ -176,59 +207,64 @@ class WhaleTranslator:
if self.vd_mode: if self.vd_mode:
self.switch_to_virtual_desktop_2() self.switch_to_virtual_desktop_2()
if not self.whale_hwnd: if not self.whale_hwnd:
# 웨일 창을 찾지 못했을 경우 사용자에게 입력 받기 # 웨일 창을 찾지 못했을 경우 사용자에게 입력 받기
self.logger.debug("웨일 창을 찾지 못했습니다. 계속하려면 'y'를 입력하세요.") self.logger.debug("웨일 창을 찾지 못했습니다. 계속하려면 'y'를 입력하세요.")
# user_input = input("계속하려면 'y'를 입력하세요: ").lower()
# if user_input != 'y':
# self.logger.debug("사용자에 의해 작업이 중단되었습니다.")
# raise SystemExit("프로그램이 사용자 요청으로 종료되었습니다.") # 프로그램 종료
# else:
# self.logger.debug("새 웨일 창을 생성합니다.")
self.create_and_update_whale_window() self.create_and_update_whale_window()
if self.whale_hwnd:
win32gui.ShowWindow(self.whale_hwnd, win32con.SW_RESTORE) # 웨일 창 활성화
win32gui.SetForegroundWindow(self.whale_hwnd)
pyautogui.moveTo(960,580) # 마우스 센터로 이동
# pyautogui.hotkey('ctrl', 'l') # 웨일 브라우저의 주소창으로 이동 if self.whale_hwnd:
self.enter_url(url) try:
pyautogui.press('enter') win32gui.ShowWindow(self.whale_hwnd, win32con.SW_RESTORE) # 웨일 창 활성화
# await asyncio.sleep(1) # 페이지 로딩 대기 win32gui.SetForegroundWindow(self.whale_hwnd)
time.sleep(2)
pyautogui.rightClick() pyautogui.moveTo(960,580) # 마우스 센터로 이동
# await asyncio.sleep(0.2) # 페이지 로딩 대기
time.sleep(2) # pyautogui.hotkey('ctrl', 'l') # 웨일 브라우저의 주소창으로 이동
self.enter_url(url)
pyautogui.press('enter')
# await asyncio.sleep(1) # 페이지 로딩 대기
time.sleep(2)
pyautogui.press('r') # 번역 클릭 pyautogui.rightClick()
# await asyncio.sleep(7) # 페이지 로딩 대기 # await asyncio.sleep(0.2) # 페이지 로딩 대기
time.sleep(7) time.sleep(2)
pyautogui.rightClick() pyautogui.press('r') # 번역 클릭
# await asyncio.sleep(0.2) # 페이지 로딩 대기 # await asyncio.sleep(7) # 페이지 로딩 대기
time.sleep(0.2) time.sleep(7)
pyautogui.press('c') # 번역된 이미지 클립보드에 복사 pyautogui.rightClick()
# pyautogui.hotkey('ctrl', 'l') # 새 탭으로 이동 # await asyncio.sleep(0.2) # 페이지 로딩 대기
# pyautogui.typewrite(self.newtab) # URL을 입력 time.sleep(0.2)
self.enter_url(self.newtab)
self.logger.debug(f'번역 완료: {url}')
if self.vd_mode: pyautogui.press('c') # 번역된 이미지 클립보드에 복사
self.return_to_virtual_desktop_1()
# 경로를 인자로 받을경우 해당경로에 파일 저장 time.sleep(1) # 클립보드 업데이트 대기
if path: if self.is_image_in_clipboard(): # 클립보드에 이미지 데이터가 있으면 성공
pass # 클립보드의 이미지를 path의 파일로 저장하고 저장경로를 리턴하는 메서드 self.translation_success_flag = True
# path에는 현재 폴더의 tmp_img폴더에 상품명-옵션명 형태로 제공됨 self.logger.debug(f'번역 성공: {url}')
self.reset_failures() # 번역 성공 시 연속 실패 횟수 초기화
else:
self.logger.error(f'번역 실패: 클립보드에 이미지 데이터가 없음')
self.handle_translation_failure()
self.enter_url(self.newtab)
self.logger.debug(f'번역 완료: {url}')
if self.vd_mode:
self.return_to_virtual_desktop_1()
# 경로를 인자로 받을경우 해당경로에 파일 저장
if path:
pass # 클립보드의 이미지를 path의 파일로 저장하고 저장경로를 리턴하는 메서드
# path에는 현재 폴더의 tmp_img폴더에 상품명-옵션명 형태로 제공됨
except Exception as e:
self.logger.error(f"번역 중 오류 발생: {e}")
self.handle_translation_failure()
else: else:
self.logger.debug('웨일 창을 찾을 수 없습니다.') self.logger.debug('웨일 창을 찾을 수 없습니다.')
self.handle_translation_failure()
def create_and_update_whale_window(self): def create_and_update_whale_window(self):
""" """