diff --git a/browser_control.py b/browser_control.py
index f010dc06..af99d0db 100644
--- a/browser_control.py
+++ b/browser_control.py
@@ -8,9 +8,10 @@ from bs4 import BeautifulSoup
import asyncio
class BrowserController:
- def __init__(self, app, logger):
+ def __init__(self, app, logger, locator_manager):
self.app = app
self.logger = logger
+ self.locator_manager = locator_manager
self.chrome_window_name = "퍼센티 - 셀러들을 위한 AI 구매대행 솔루션 - Chrome"
# self.whale_window_name = "새 시크릿 탭 - Whale"
self.chrome_hwnd = None
@@ -20,6 +21,29 @@ class BrowserController:
self.browser = 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):
return self.page
@@ -62,19 +86,19 @@ class BrowserController:
if is_admin:
# 관리자 로그인 처리
- await self.page.fill('input[placeholder="이메일 주소 입력"]', admin_id) # 관리자 ID 입력
- await self.page.fill('input[placeholder="영문/숫자/특수문자의 조합 (6~15자리)"]', admin_password) # 관리자 비밀번호 입력
- await self.page.click('button:has-text("로그인 하기")') # 관리자 로그인 버튼 클릭
+ await self.page.fill(self.login_email_locator, admin_id) # 관리자 ID 입력
+ await self.page.fill(self.login_password_locator, admin_password) # 관리자 비밀번호 입력
+ await self.page.click(self.login_button_locator) # 관리자 로그인 버튼 클릭
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":
await admin_toggle.click() # 관리자 모드에서 직원 모드로 전환
- await self.page.fill('input[placeholder="이메일 주소 입력"]', admin_id) # 관리자 ID 입력
- await self.page.fill('input[placeholder="직원 아이디 입력"]', user_id) # 직원 ID 입력
- await self.page.fill('input[placeholder="영문/숫자/특수문자의 조합 (6~15자리)"]', user_password) # 직원 비밀번호 입력
- await self.page.click('button:has-text("직원 로그인 하기")') # 직원 로그인 버튼 클릭
+ await self.page.fill(self.login_email_locator, admin_id) # 관리자 ID 입력
+ await self.page.fill(self.staff_id_locator, user_id) # 직원 ID 입력
+ await self.page.fill(self.login_password_locator, user_password) # 직원 비밀번호 입력
+ await self.page.click(self.staff_login_button_locator) # 직원 로그인 버튼 클릭
self.logger.debug(f'로그인 완료: {"관리자" if is_admin else "직원"} 계정')
@@ -82,7 +106,6 @@ class BrowserController:
await self.close_ad_if_exists()
-
async def close_browser(self):
"""브라우저 종료"""
if self.browser:
@@ -161,40 +184,41 @@ class BrowserController:
def fetch_image_urls(self, html_content):
"""
- HTML 콘텐츠에서 모든
태그의 URL을 순서대로 추출
+ HTML 콘텐츠에서 모든
태그의 URL을 순서대로 추출하고 중복 제거.
"""
soup = BeautifulSoup(html_content, 'html.parser')
- image_urls = []
+
+ # 중복된 이미지를 제거하기 위해 set 사용
+ image_urls_set = set()
# class="image_resized"를 가진 모든
태그 찾기
images_resized = soup.find_all('img', class_='image_resized')
for img in images_resized:
- if img and 'src' in img.attrs and img['src'] not in image_urls:
- image_urls.append(img['src'])
+ if img and 'src' in img.attrs:
+ image_urls_set.add(img['src']) # 중복을 방지하기 위해 set에 추가
# 내부의 모든
태그 찾기
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 and img_tag['src'] not in image_urls:
- image_urls.append(img_tag['src'])
+ if img_tag and 'src' in img_tag.attrs:
+ image_urls_set.add(img_tag['src']) # 중복을 방지하기 위해 set에 추가
+ # set을 list로 변환하여 반환 (순서 유지가 필요하면 set 대신 리스트로 처리해야 함)
+ image_urls = list(image_urls_set)
return image_urls
+
async def close_ad_if_exists(self):
"""광고 다이얼로그가 있으면 닫기 버튼을 클릭하는 메서드"""
try:
# 광고 다이얼로그가 나타날 때까지 기다림
- dialog_selector = "div.ant-modal-wrap.ant-modal-centered"
- 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)
+ await self.page.wait_for_selector(self.close_ad_dialog_locator, timeout=3000)
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:
await close_button.click()
self.logger.debug("다이얼로그를 성공적으로 닫았습니다.")
@@ -208,7 +232,8 @@ class BrowserController:
async def go_to_new_product_page(self):
"""신규 상품 등록 페이지로 이동"""
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("신규 상품 등록 페이지로 이동 완료.")
except Exception as e:
self.logger.debug(f"신규 상품 등록 페이지 이동 중 오류: {e}", exc_info=True)
@@ -269,103 +294,59 @@ class BrowserController:
except Exception as e:
self.logger.debug(f"옵션 탭 클릭 중 오류: {e}", exc_info=True)
- # async def extract_image_urls(self):
- # """HTML에서 이미지 URL 추출 및 img 태그 삭제 후 소스 버튼 다시 클릭"""
- # self.logger.debug('이미지 URL을 추출 중...')
-
- # # 소스 버튼 클릭
- # await self.page.click("button[data-cke-tooltip-text='소스']")
- # self.logger.debug('소스 버튼 클릭 완료.')
-
- # # '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 click_price_tab(self):
+ """상세페이지 탭 클릭"""
+ try:
+ await self.page.click('div.ant-tabs-tab:has-text("가격")')
+ self.logger.debug("가격 탭 클릭 완료.")
+ except Exception as e:
+ self.logger.debug(f"가격 탭 클릭 중 오류: {e}", exc_info=True)
async def extract_image_urls(self, optionHandler, is_option_data=False):
"""상세페이지에서 이미지 URL 추출"""
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("소스 버튼 클릭 완료.")
+
# '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")
+
# HTML 소스에서 이미지 URL 추출
image_urls = self.fetch_image_urls(data_value)
self.logger.debug(f'추출된 이미지 URL 수: {len(image_urls)}')
# HTML 소스에서 이미지 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')
+ data_value_element = await self.page.query_selector(ck_source_editing_area_locator)
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}")')
- # 데이터가 제대로 변경되었는지 확인
+ 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('button[data-cke-tooltip-text="소스"]')
+ await self.page.click(source_button_locator)
self.logger.debug('소스 버튼 재 클릭 완료.')
+
if is_option_data:
self.logger.debug('옵션 데이터 입력 시작')
option_data = optionHandler.get_selected_translated_options()
# 옵션 입력 필드 선택
- input_field_selector = '//*[@id="productMainContentContainerId"]/div/div/div[2]/div[2]/div[2]/div' # 여기에 적절한 입력 필드의 셀렉터를 입력
- input_field = await self.page.wait_for_selector(input_field_selector)
-
+ input_field_locator = self.locator_manager.get_locator('BrowserControl', 'option_input_field_locator')
+ input_field = await self.page.wait_for_selector(input_field_locator)
# 선두부 텍스트 입력
await input_field.type('---')
@@ -446,7 +427,8 @@ class BrowserController:
"""다음 페이지로 이동"""
try:
# 현재 페이지가 몇 번째 페이지인지 확인 (클래스에 '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:
self.logger.debug("현재 페이지 정보를 찾을 수 없습니다.")
@@ -457,7 +439,9 @@ class BrowserController:
next_page_number = current_page_number + 1
# 다음 페이지 버튼을 찾음 (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:
await next_page_button.click() # 페이지 버튼 클릭
@@ -481,16 +465,11 @@ class BrowserController:
win32gui.ShowWindow(self.chrome_hwnd, win32con.SW_RESTORE)
win32gui.SetForegroundWindow(self.chrome_hwnd)
self.logger.debug('크롬 창으로 포커스 이동.')
- self.logger.debug('크롬 창으로 포커스 이동.')
else:
self.logger.debug('크롬 창을 찾을 수 없습니다.')
- self.logger.debug('크롬 창을 찾을 수 없습니다.')
except Exception as e:
self.logger.debug(f"크롬 포커스 전환 중 오류: {e}", exc_info=True)
-
-
-
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까지 상품 처리
try:
# 각 상품의 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_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_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_name_locator = self.product_name_template.format(i=i)
+ product_price_locator = self.product_price_template.format(i=i)
+ product_image_locator = self.product_image_template.format(i=i)
- product_name_element = self.page.locator(product_name_selector)
- product_price_element = self.page.locator(product_price_selector)
- product_image_element = self.page.locator(product_image_selector)
+ product_name_element = await self.page.query_selector(product_name_locator)
+ product_price_element = await self.page.query_selector(product_price_locator)
+ product_image_element = await self.page.query_selector(product_image_locator)
if product_name_element and product_price_element and product_image_element:
product_info = {
diff --git a/clipboardImageManager.py b/clipboardImageManager.py
index 9a6248ed..55f9660e 100644
--- a/clipboardImageManager.py
+++ b/clipboardImageManager.py
@@ -11,6 +11,7 @@ import os
from datetime import datetime
import random
import asyncio
+import pyperclip # 클립보드 데이터를 확인하기 위한 라이브러리
class ClipboardImageManager:
def __init__(self, app, logger, browser_controller, debug=False):
diff --git a/config.ini b/config.ini
new file mode 100644
index 00000000..93e26437
--- /dev/null
+++ b/config.ini
@@ -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
diff --git a/gui.py b/gui.py
index b0046aac..17735b34 100644
--- a/gui.py
+++ b/gui.py
@@ -6,6 +6,8 @@ from whale_translator import WhaleTranslator
from clipboardImageManager import ClipboardImageManager
from vertexAI import VertexAITranslator
from option import OptionHandler
+from price import PriceHandler
+from locatorManager import LocatorManager
from logger_module import QTextEditLogger # 추가
import logging
import asyncio
@@ -18,7 +20,8 @@ class TranslationApp(QWidget):
self.debug = False
key_path = 'leensoo1nt.json'
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 = whale_translator
@@ -26,7 +29,8 @@ class TranslationApp(QWidget):
self.optionHandler = None
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
@@ -406,6 +410,11 @@ class TranslationApp(QWidget):
await self.edit_option(product_name)
self.complete_stage(0)
+ # 가격 수정
+ # self.start_stage(0)
+ await self.edit_price()
+ # self.complete_stage(0)
+
# 상세페이지 수정
self.start_stage(1)
await self.detail_trans()
@@ -580,12 +589,28 @@ class TranslationApp(QWidget):
self.detail_progress_bar.setVisible(True)
# 옵션 최대선택갯수
- max_option_count = 10
+ max_option_count = 20
option_image_trans = False
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)
+
+ 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)
+
diff --git a/locatorManager.py b/locatorManager.py
new file mode 100644
index 00000000..12438a11
--- /dev/null
+++ b/locatorManager.py
@@ -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
diff --git a/main.py b/main.py
index 6e8013e3..62202d91 100644
--- a/main.py
+++ b/main.py
@@ -48,7 +48,7 @@ async def main():
logger.error(f"DPI 인식 설정 실패: {e}")
# 기존 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()
window = TranslationApp(logger, whale_translator) # PySide6 UI
window.show()
diff --git a/option.py b/option.py
index f365dc31..438688a8 100644
--- a/option.py
+++ b/option.py
@@ -5,7 +5,8 @@ import numpy as np
import asyncio
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.page = self.browser_controller.page
self.logger = logger
diff --git a/price.py b/price.py
new file mode 100644
index 00000000..022a5355
--- /dev/null
+++ b/price.py
@@ -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
diff --git a/price_ori.py b/price_ori.py
new file mode 100644
index 00000000..5e53a20a
--- /dev/null
+++ b/price_ori.py
@@ -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
diff --git a/whale_translator.py b/whale_translator.py
index 84bbeb51..12e68a4f 100644
--- a/whale_translator.py
+++ b/whale_translator.py
@@ -6,9 +6,10 @@ from pyvda import VirtualDesktop, get_virtual_desktops
import subprocess
import asyncio
import KO_EN
+import pyperclip # 클립보드 데이터를 확인하기 위한 라이브러리
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.logger = logger
self.vd_mode = vd_mode
@@ -16,6 +17,10 @@ class WhaleTranslator:
self.whale_pid = None
isSecret = secret_mode
+ self.translation_success_flag = False # 번역 성공 플래그
+ self.failure_count = 0 # 실패 횟수
+ self.max_failures = max_failures # 최대 실패 횟수
+
if isSecret:
self.whale_window_name = "새 시크릿 탭 - Whale"
else:
@@ -69,6 +74,32 @@ class WhaleTranslator:
if self.vd_mode:
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):
"""웨일 창 핸들을 찾는 메서드"""
@@ -176,59 +207,64 @@ class WhaleTranslator:
if self.vd_mode:
self.switch_to_virtual_desktop_2()
-
if not self.whale_hwnd:
# 웨일 창을 찾지 못했을 경우 사용자에게 입력 받기
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()
-
-
- 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') # 웨일 브라우저의 주소창으로 이동
- self.enter_url(url)
- pyautogui.press('enter')
- # await asyncio.sleep(1) # 페이지 로딩 대기
- time.sleep(2)
+ if self.whale_hwnd:
+ try:
+ win32gui.ShowWindow(self.whale_hwnd, win32con.SW_RESTORE) # 웨일 창 활성화
+ win32gui.SetForegroundWindow(self.whale_hwnd)
- pyautogui.rightClick()
- # await asyncio.sleep(0.2) # 페이지 로딩 대기
- time.sleep(2)
+ pyautogui.moveTo(960,580) # 마우스 센터로 이동
+
+ # pyautogui.hotkey('ctrl', 'l') # 웨일 브라우저의 주소창으로 이동
+ self.enter_url(url)
+ pyautogui.press('enter')
+ # await asyncio.sleep(1) # 페이지 로딩 대기
+ time.sleep(2)
- pyautogui.press('r') # 번역 클릭
- # await asyncio.sleep(7) # 페이지 로딩 대기
- time.sleep(7)
+ pyautogui.rightClick()
+ # await asyncio.sleep(0.2) # 페이지 로딩 대기
+ time.sleep(2)
- pyautogui.rightClick()
- # await asyncio.sleep(0.2) # 페이지 로딩 대기
- time.sleep(0.2)
+ pyautogui.press('r') # 번역 클릭
+ # await asyncio.sleep(7) # 페이지 로딩 대기
+ time.sleep(7)
- pyautogui.press('c') # 번역된 이미지 클립보드에 복사
- # pyautogui.hotkey('ctrl', 'l') # 새 탭으로 이동
- # pyautogui.typewrite(self.newtab) # URL을 입력
- self.enter_url(self.newtab)
- self.logger.debug(f'번역 완료: {url}')
+ pyautogui.rightClick()
+ # await asyncio.sleep(0.2) # 페이지 로딩 대기
+ time.sleep(0.2)
- if self.vd_mode:
- self.return_to_virtual_desktop_1()
+ pyautogui.press('c') # 번역된 이미지 클립보드에 복사
- # 경로를 인자로 받을경우 해당경로에 파일 저장
- if path:
- pass # 클립보드의 이미지를 path의 파일로 저장하고 저장경로를 리턴하는 메서드
- # path에는 현재 폴더의 tmp_img폴더에 상품명-옵션명 형태로 제공됨
+ time.sleep(1) # 클립보드 업데이트 대기
+ if self.is_image_in_clipboard(): # 클립보드에 이미지 데이터가 있으면 성공
+ self.translation_success_flag = True
+ 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:
self.logger.debug('웨일 창을 찾을 수 없습니다.')
+ self.handle_translation_failure()
def create_and_update_whale_window(self):
"""