This commit is contained in:
parent
6be03a3c0b
commit
d8a7aecf7f
|
|
@ -0,0 +1,26 @@
|
|||
옵션번역을 위한 vertext API키 설정
|
||||
옵션선택 기준 : 최대 옵션갯수, 옵션이미지번역유무, 옵션AI선택유무 , 옵션필터링 기준(표준편차범위등 ) 기본동작설명
|
||||
가격조정 기준 : 카드수수료 설정, 기본마진 설정, 기본더하기마진과 해외배송비 설정, 적정가격 산출비율(팔린가격, 원가x2, ) 카테고리별 해외배송비 가중치 사용유무
|
||||
더하기마진과, 해외배송비의 기본가중치 설정
|
||||
상세페이지 기준 : 옵션명 입력 유무, 상세설명 유무 및 설정버튼, 상세페이지 이미지번역설정, 이미지번역방법(자체번역1(기본인페인팅+deepL), 자체번역2(AI인페인팅+deepL), 웨일번역VD모드, 웨일번역기본모드)
|
||||
|
||||
상품명 기준 : 네이버 키워드 상품정보 수집으로 자동키워드조합
|
||||
형태소 분석을 활용한 키워드 조합
|
||||
태그 기준 : 태그사용 유무
|
||||
썸네일 기준 : 썸네일 번역기준
|
||||
|
||||
로거의 디버그와 인포 구분
|
||||
|
||||
유저설정을 db에 통합
|
||||
|
||||
프로그램 로그인연동
|
||||
|
||||
FastAPI연동
|
||||
|
||||
상품명 AI 작성
|
||||
인자목록 : 원본상품명, 판매키워드, 네이버 상위판매상품의 상품명 20여개.등을 제공.
|
||||
네이버 노출로직 제공하여 vertexai로 수행
|
||||
상품명 프롬프트 "한글로 된 단어와 영어 또는 영어와 숫자로된 상품코드, 전압, 전류 또는 여러가지 스펙등을 인식할수 있고, 각 옵션별로 특징적인 내용만 남기는 방법을 사용하려면??
|
||||
상품명에서 어느정도 옵션이 어떤내용을 말하는지를 유추할수 있잖니? 그래서 상품명과 옵션명들을 함께 제공할꺼야.
|
||||
물론 중국어로 된 원문이야.
|
||||
그래서 각 옵션별 특징을 간결하게 남긴 뒤 한글로 번역하는게 목표야."
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
6473
appTranslator.log
6473
appTranslator.log
File diff suppressed because it is too large
Load Diff
|
|
@ -170,7 +170,7 @@ class BrowserController:
|
|||
}}''', self.total_product_count_locator)
|
||||
|
||||
if element_text:
|
||||
self.logger.debug(f"가져온 텍스트: {element_text}") # 텍스트 확인용 로그
|
||||
self.logger.debug(f"총 상품수 확인: {element_text}") # 텍스트 확인용 로그
|
||||
# "총 xx개 상품"에서 숫자만 추출
|
||||
count = int(''.join(filter(str.isdigit, element_text)))
|
||||
return count
|
||||
|
|
|
|||
24
config.ini
24
config.ini
|
|
@ -5,26 +5,28 @@ exchange_fee_input_locator = '//*[@id='productMainContentContainerId']/div/div[1
|
|||
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'
|
||||
product_cost_locator = '//*[@id='productMainContentContainerId']/div/div[2]/div/div/div[5]/div[1]/div/div/div/div/div[2]/table/tbody/tr[{index}]/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[{index}]/td[4]/div/div/div[1]/div/div[2]/input'
|
||||
|
||||
[OptionLocators]
|
||||
# 옵션 관련 선택자
|
||||
option_excluded_selector_template = '//*[@id="productMainContentContainerId"]/div[1]/div[2]/div/div/div[2]/div/div[1]/div/div/div[2]/div/div/div[5]/div[1]/div/div/ul/li[{i}]/div/div[1]/div/div[2]/div/div[3]'
|
||||
option_input_selector_template = '//*[@id="productMainContentContainerId"]/div[1]/div[2]/div/div/div[2]/div/div[1]/div/div/div[2]/div/div/div[5]/div[1]/div/div/ul/li[{i}]/div/div[1]/div/div[3]/div[2]/div[1]/span/input'
|
||||
option_excluded_selector_template = '//*[@id="productMainContentContainerId"]/div[1]/div[2]/div/div/div[2]/div/div[1]/div/div/div[2]/div/div/div[5]/div[1]/div/div/ul/li[{index}]/div/div[1]/div/div[2]/div/div[3]'
|
||||
option_input_selector_template = '//*[@id="productMainContentContainerId"]/div[1]/div[2]/div/div/div[2]/div/div[1]/div/div/div[2]/div/div/div[5]/div[1]/div/div/ul/li[{index}]/div/div[1]/div/div[3]/div[2]/div[1]/span/input'
|
||||
single_option_locator = '//div[@id="productMainContentContainerId"]//label[contains(@class, 'ant-radio-button-wrapper-checked') and contains(., '단일 상품등록')]'
|
||||
option_product_locator = '//div[@id="productMainContentContainerId"]//label[contains(@class, 'ant-radio-button-wrapper-checked') and contains(., '옵션 상품등록')]'
|
||||
total_options_selector = '#productMainContentContainerId label.ant-checkbox-wrapper'
|
||||
is_all_option_checked_selector = '#productMainContentContainerId .ant-checkbox-indeterminate'
|
||||
original_name_selector_template = 'div#productMainContentContainerId li:nth-child({i}) > div > div:nth-child(1) > div > div:nth-child(3) > div:nth-child(3) > span'
|
||||
edit_field_selector_template = 'div#productMainContentContainerId li:nth-child({i}) > div > div:nth-child(1) > div > div:nth-child(3) > div:nth-child(2) > div:nth-child(1) > span > input'
|
||||
checkbox_selector_template = '#productMainContentContainerId li:nth-child({i}) input[type="checkbox"]'
|
||||
image_selector_template = '#productMainContentContainerId li:nth-child({i}) img.sc-gbvfcU.ezktkd'
|
||||
price_selector_template = '#productMainContentContainerId li:nth-child({i}) sup'
|
||||
delete_button_selector_template = '#productMainContentContainerId > div.sc-TOgAA.fZvEqY > div:nth-child(2) > div > div > div:nth-child(2) > div > div.sc-cFShuL.dbIeho > div > div > div.ant-collapse-content.ant-collapse-content-active > div > div > div.sc-fGdiLE.iyXMeU > div.ant-list.ant-list-split.css-1li46mu > div > div > ul > li:nth-child({i}) > div > div:nth-child(1) > div > div:nth-child(2) > div > div.ant-row.ant-row-no-wrap.ant-row-space-between.ant-row-middle.css-1li46mu > div:nth-child(1) > div'
|
||||
original_name_selector_template = 'div#productMainContentContainerId li:nth-child({index}) > div > div:nth-child(1) > div > div:nth-child(3) > div:nth-child(3) > span'
|
||||
edit_field_selector_template = 'div#productMainContentContainerId li:nth-child({index}) > div > div:nth-child(1) > div > div:nth-child(3) > div:nth-child(2) > div:nth-child(1) > span > input'
|
||||
checkbox_selector_template = '#productMainContentContainerId li:nth-child({index}) input[type="checkbox"]'
|
||||
image_selector_template = '#productMainContentContainerId li:nth-child({index}) img.sc-gbvfcU.ezktkd'
|
||||
price_selector_template = '#productMainContentContainerId li:nth-child({index}) sup'
|
||||
delete_button_selector_template = '#productMainContentContainerId > div.sc-TOgAA.fZvEqY > div:nth-child(2) > div > div > div:nth-child(2) > div > div.sc-cFShuL.dbIeho > div > div > div.ant-collapse-content.ant-collapse-content-active > div > div > div.sc-fGdiLE.iyXMeU > div.ant-list.ant-list-split.css-1li46mu > div > div > ul > li:nth-child({index}) > div > div:nth-child(1) > div > div:nth-child(2) > div > div.ant-row.ant-row-no-wrap.ant-row-space-between.ant-row-middle.css-1li46mu > div:nth-child(1) > div'
|
||||
confirm_delete_button_locator = 'body > div:nth-child(18) > div > div.ant-modal-wrap.ant-modal-confirm-centered.ant-modal-centered > div > div.sc-ddjGPC.jbwEYW > div > div > div > div.ant-modal-confirm-btns > button.ant-btn.css-1li46mu.ant-btn-primary.ant-btn-dangerous'
|
||||
add_button_selector_template = '#productMainContentContainerId > div.sc-TOgAA.fZvEqY > div:nth-child(2) > div > div > div:nth-child(2) > div > div.sc-cFShuL.dbIeho > div > div > div.ant-collapse-content.ant-collapse-content-active > div > div > div.sc-fGdiLE.iyXMeU > div.ant-list.ant-list-split.css-1li46mu > div > div > ul > li:nth-child({i}) > div > div:nth-child(1) > div > div:nth-child(2) > div > div > img'
|
||||
add_button_selector_template = '#productMainContentContainerId > div.sc-TOgAA.fZvEqY > div:nth-child(2) > div > div > div:nth-child(2) > div > div.sc-cFShuL.dbIeho > div > div > div.ant-collapse-content.ant-collapse-content-active > div > div > div.sc-fGdiLE.iyXMeU > div.ant-list.ant-list-split.css-1li46mu > div > div > ul > li:nth-child({index}) > div > div:nth-child(1) > div > div:nth-child(2) > div > div > img'
|
||||
file_input_locator = 'input[type="file"]'
|
||||
low_order_button_locator = 'button:has-text("가격 낮은 순")'
|
||||
AtoZ_button_locator = 'button:has-text("A-Z")'
|
||||
|
||||
[DetailLocators]
|
||||
product_detail_input_locator = '//*[@id='detailMainContainerId']/div/div/div[{i}]/textarea'
|
||||
|
|
|
|||
24
gui.py
24
gui.py
|
|
@ -10,9 +10,11 @@ from price import PriceHandler
|
|||
from title import TitleHandler
|
||||
from locatorManager import LocatorManager
|
||||
from src.cmb_diag import CMBSettingsDialog
|
||||
from src.DatabaseManager import DatabaseManager
|
||||
from logger_module import QTextEditLogger # 추가
|
||||
import logging
|
||||
import asyncio
|
||||
import os, shutil
|
||||
|
||||
class TranslationApp(QWidget):
|
||||
def __init__(self, logger=None, app=None):
|
||||
|
|
@ -32,7 +34,23 @@ class TranslationApp(QWidget):
|
|||
self.vertexAI = VertexAITranslator(self.logger, key_path)
|
||||
self.optionHandler = None
|
||||
|
||||
self.cmb_diag = CMBSettingsDialog(parent=self, logger=self.logger, debug=True)
|
||||
# DB 파일 경로 설정
|
||||
self.base_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
self.user_db_path = os.path.join(self.base_dir, "userDB.db")
|
||||
self.initial_db_path = os.path.join(self.base_dir, "src", "initialDB.db")
|
||||
|
||||
# userDB.db 파일이 없으면 initialDB.db를 복사해서 생성
|
||||
if not os.path.exists(self.user_db_path):
|
||||
if os.path.exists(self.initial_db_path):
|
||||
shutil.copyfile(self.initial_db_path, self.user_db_path)
|
||||
print("initialDB.db를 userDB.db로 복사했습니다.")
|
||||
else:
|
||||
raise FileNotFoundError("initialDB.db 파일이 없습니다. 초기 DB 파일이 존재하는지 확인해주세요.")
|
||||
|
||||
# DatabaseManager 초기화
|
||||
self.db_manager = DatabaseManager(db_url=f"sqlite:///{self.user_db_path}", logger=self.logger)
|
||||
|
||||
self.cmb_diag = CMBSettingsDialog(parent=self, logger=self.logger, db_manager=self.db_manager, initial_db_path=self.initial_db_path, user_db_path=self.user_db_path, debug=self.debug)
|
||||
self.clipboardImageManager = ClipboardImageManager(self, logger, self.browser_controller, 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.cmb_diag, self.debug)
|
||||
|
|
@ -698,7 +716,7 @@ class TranslationApp(QWidget):
|
|||
self.complete_stage(0)
|
||||
|
||||
if self.toggle_states['optionTrnas'] or self.toggle_states['optionIMGTrans'] or self.toggle_states['optionAutoSelect']:
|
||||
self.logger.debug(f"옵션수정 : {self.toggle_states['optionTrnas']} + {self.toggle_states['optionIMGTrans']} + {self.toggle_states['optionAutoSelect']}")
|
||||
self.logger.debug(f"옵션수정 : optionTrnas={self.toggle_states['optionTrnas']} + optionIMGTrans={self.toggle_states['optionIMGTrans']} + optionAutoSelect{self.toggle_states['optionAutoSelect']}")
|
||||
# 옵션 수정
|
||||
self.start_stage(0)
|
||||
await self.edit_option(product_name)
|
||||
|
|
@ -811,7 +829,7 @@ class TranslationApp(QWidget):
|
|||
# 옵션 최대선택갯수
|
||||
max_option_count = 20
|
||||
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, self.toggle_states)
|
||||
|
||||
# 수정 후 저장
|
||||
# await self.optionHandler.save_option()
|
||||
|
|
|
|||
|
|
@ -73,6 +73,8 @@ class LocatorManager:
|
|||
'confirm_delete_button_locator': self.config.get('OptionLocators', 'confirm_delete_button_locator').strip("'"),
|
||||
'add_button_selector_template': self.config.get('OptionLocators', 'add_button_selector_template').strip("'"),
|
||||
'file_input_locator': self.config.get('OptionLocators', 'file_input_locator').strip("'"),
|
||||
'low_order_button_locator': self.config.get('OptionLocators', 'low_order_button_locator').strip("'"),
|
||||
'AtoZ_button_locator': self.config.get('OptionLocators', 'AtoZ_button_locator').strip("'"),
|
||||
}
|
||||
|
||||
# TitleLocators 섹션
|
||||
|
|
|
|||
130
option.py
130
option.py
|
|
@ -2,7 +2,7 @@ from collections import Counter
|
|||
import pyautogui
|
||||
from datetime import datetime
|
||||
import numpy as np
|
||||
import asyncio
|
||||
import asyncio, time, math
|
||||
|
||||
class OptionHandler:
|
||||
def __init__(self, locator_manager, browser_controller, whale_translator, logger, vertexAI, debug_flag=False):
|
||||
|
|
@ -31,6 +31,8 @@ class OptionHandler:
|
|||
self.confirm_delete_button_locator = self.locator_manager.get_locator('OptionLocators', 'confirm_delete_button_locator')
|
||||
self.add_button_selector_template = self.locator_manager.get_locator('OptionLocators', 'add_button_selector_template')
|
||||
self.file_input_locator = self.locator_manager.get_locator('OptionLocators', 'file_input_locator')
|
||||
self.low_order_button_locator = self.locator_manager.get_locator('OptionLocators', 'low_order_button_locator')
|
||||
self.AtoZ_button_locator = self.locator_manager.get_locator('OptionLocators', 'AtoZ_button_locator')
|
||||
|
||||
def update_page(self, page1):
|
||||
self.page = page1
|
||||
|
|
@ -81,7 +83,12 @@ class OptionHandler:
|
|||
std_price = np.std(prices)
|
||||
|
||||
self.logger.debug(f"최저옵션: {mean_price}, 표준편차: {std_price}")
|
||||
|
||||
|
||||
if std_price == 0:
|
||||
# 모든 옵션의 가격이 동일한 경우 그대로 반환
|
||||
self.logger.debug("모든 옵션의 가격이 동일합니다. 필터링 없이 모든 옵션을 반환합니다.")
|
||||
return options
|
||||
|
||||
filtered_options = []
|
||||
for option in options:
|
||||
z_score = (option['price'] - mean_price) / std_price
|
||||
|
|
@ -102,8 +109,7 @@ class OptionHandler:
|
|||
# 최종적으로 필터링된 가격 범위 내에 있는 옵션만 선택
|
||||
final_options = [option for option in filtered_options if lower_bound <= option['price'] <= upper_bound]
|
||||
|
||||
self.logger.debug(f"최종 선택된 옵션: {[opt['price'] for opt in final_options]}")
|
||||
|
||||
self.logger.debug(f"최종 선택된 옵션: {[(opt['name'], opt['price']) for opt in final_options]}")
|
||||
return final_options
|
||||
|
||||
async def store_selected_options(self):
|
||||
|
|
@ -111,13 +117,13 @@ class OptionHandler:
|
|||
try:
|
||||
selected_translated_options = []
|
||||
|
||||
total_options_count = len(self.option_info['original_names'])
|
||||
total_options_count = len([self.option_info]['original_names'])
|
||||
|
||||
await self.low_order_click()
|
||||
|
||||
for i in range(1, total_options_count + 1):
|
||||
option_excluded_selector = self.option_excluded_selector_template.format(i)
|
||||
option_input_selector = self.option_input_selector_template.format(i)
|
||||
option_excluded_selector = self.option_excluded_selector_template.format(index=i)
|
||||
option_input_selector = self.option_input_selector_template.format(index=i)
|
||||
|
||||
option_excluded_element = await self.page.query_selector(option_excluded_selector)
|
||||
if not option_excluded_element:
|
||||
|
|
@ -135,7 +141,7 @@ class OptionHandler:
|
|||
self.logger.error(f"선택된 옵션 저장 중 오류 발생: {e}", exc_info=True)
|
||||
|
||||
|
||||
async def process_options(self, product_name, max_option_count=20, option_image_trans_flag=False):
|
||||
async def process_options(self, product_name, max_option_count=20, toggle_states=False):
|
||||
"""
|
||||
옵션 처리 로직. 옵션을 번역하고 이미지를 업데이트함.
|
||||
|
||||
|
|
@ -171,36 +177,44 @@ class OptionHandler:
|
|||
await self.low_order_click()
|
||||
|
||||
# 4. 옵션 정보 수집 및 번역
|
||||
self.option_info = await self.collect_options_info()
|
||||
if toggle_states['optionTrnas']:
|
||||
self.logger.debug(f"옵션 AI번역 : {toggle_states['optionTrnas']}")
|
||||
self.option_info = await self.collect_options_info()
|
||||
|
||||
translation_success = False # 성공/실패 플래그
|
||||
translation_success = False # 성공/실패 플래그
|
||||
|
||||
try:
|
||||
# Vertex AI를 통한 번역 시도
|
||||
translated_options = await self.vertexAItranslator.translate_options(self.option_info['original_names'], product_name)
|
||||
self.logger.debug(f"번역된 옵션 입력")
|
||||
await self.apply_translated_options(translated_options, self.option_info['edit_fields'])
|
||||
try:
|
||||
# Vertex AI를 통한 번역 시도
|
||||
translated_options = await self.vertexAItranslator.translate_options(self.option_info['original_names'], product_name)
|
||||
self.logger.debug(f"번역된 옵션 입력")
|
||||
await self.apply_translated_options(translated_options, self.option_info['edit_fields'])
|
||||
|
||||
translation_success = True # 번역 성공
|
||||
translation_success = True # 번역 성공
|
||||
|
||||
except ValueError as ve:
|
||||
if "SAFETY" in str(ve):
|
||||
self.logger.error(f"안전 필터에 의해 번역 요청이 차단되었습니다. {ve}")
|
||||
self.logger.debug(f"퍼센티 자체 AI번역 사용 시도")
|
||||
pyautogui.hotkey('alt', 'q')
|
||||
translation_success = False # 번역 실패
|
||||
except ValueError as ve:
|
||||
if "SAFETY" in str(ve):
|
||||
self.logger.error(f"안전 필터에 의해 번역 요청이 차단되었습니다. {ve}")
|
||||
self.logger.debug(f"퍼센티 자체 AI번역 사용 시도")
|
||||
pyautogui.hotkey('alt', 'q')
|
||||
self.logger.debug(f"번역을 위한 5초간 대기")
|
||||
# await asyncio.sleep(5)
|
||||
time.sleep(5)
|
||||
translation_success = False # 번역 실패
|
||||
|
||||
self.logger.debug(f"[{'VertexAI' if translation_success else '퍼센티AI'}] 를 이용한 옵션번역 성공")
|
||||
|
||||
# 5. 옵션 필터링 및 조정
|
||||
self.logger.debug(f"옵션 필터링 및 조정")
|
||||
await self.filter_and_adjust_options(max_option_count)
|
||||
if toggle_states['optionAutoSelect']:
|
||||
self.logger.debug(f"옵션 필터링 및 조정 : {toggle_states['optionAutoSelect']}")
|
||||
await self.filter_and_adjust_options(max_option_count)
|
||||
|
||||
# 6. 선택된 옵션 재수집
|
||||
self.logger.debug(f"옵션 필터링 및 조정")
|
||||
await self.store_selected_options() # 페이지에서 실제 선택된 옵션을 수집하여 저장
|
||||
|
||||
# 7. 옵션 이미지 업데이트 (옵션 이미지가 있는 경우)
|
||||
if option_image_trans_flag:
|
||||
self.logger.debug("옵션 이미지 업데이트 (옵션 이미지가 있는 경우만)")
|
||||
if toggle_states['optionIMGTrans']:
|
||||
self.logger.debug(f"옵션 이미지번역(옵션 이미지가 있는 경우만) : {toggle_states['optionIMGTrans']}")
|
||||
for index, option_image_url in enumerate(self.option_info.get('option_images', []), start=1):
|
||||
option_name = translated_options.get(f'trans_option_{index}', f'옵션_{index}')
|
||||
await self.update_option_image(index, option_image_url, product_name, option_name, self.debug_flag)
|
||||
|
|
@ -283,11 +297,11 @@ class OptionHandler:
|
|||
# 옵션 정보를 비동기로 수집 (각 항목 병렬 처리)
|
||||
for i in range(1, total_options_count + 1):
|
||||
try:
|
||||
original_name_selector = self.original_name_selector_template.format(i)
|
||||
edit_field_selector = self.edit_field_selector_template.format(i)
|
||||
checkbox_selector = self.checkbox_selector_template.format(i)
|
||||
image_selector = self.image_selector_template.format(i)
|
||||
price_selector = self.price_selector_template.format(i)
|
||||
original_name_selector = self.original_name_selector_template.format(index=i)
|
||||
edit_field_selector = self.edit_field_selector_template.format(index=i)
|
||||
checkbox_selector = self.checkbox_selector_template.format(index=i)
|
||||
image_selector = self.image_selector_template.format(index=i)
|
||||
price_selector = self.price_selector_template.format(index=i)
|
||||
|
||||
tasks = [
|
||||
self.page.query_selector(original_name_selector),
|
||||
|
|
@ -435,15 +449,15 @@ class OptionHandler:
|
|||
|
||||
if edit_field:
|
||||
await edit_field.fill(translated_name) # 필드에 번역된 옵션명 입력
|
||||
self.logger.debug(f"{key}번째 translated_name : [{translated_name}] 입력 완료")
|
||||
self.logger.info(f"{key}번째 translated_name : [{translated_name}] 입력 완료")
|
||||
|
||||
# 업데이트할 번역된 이름을 임시 딕셔너리에 저장
|
||||
updated_translations[original_name] = translated_name
|
||||
|
||||
else:
|
||||
self.logger.debug(f"{key}번째 옵션 필드가 없습니다.")
|
||||
self.logger.error(f"{key}번째 옵션 필드가 없습니다.")
|
||||
else:
|
||||
self.logger.debug(f"원본 옵션명을 찾을 수 없습니다: {origin_option_key}")
|
||||
self.logger.error(f"원본 옵션명을 찾을 수 없습니다: {origin_option_key}")
|
||||
|
||||
# 모든 번역이 끝난 후 한 번에 업데이트
|
||||
self.option_info['selected_translated_options'].update(updated_translations)
|
||||
|
|
@ -452,6 +466,20 @@ class OptionHandler:
|
|||
except Exception as e:
|
||||
self.logger.error(f"번역된 옵션명을 입력하는 중 오류 발생: {e}", exc_info=True)
|
||||
|
||||
def round_to_UP(self, 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
|
||||
|
||||
async def filter_and_adjust_options(self, max_option_count):
|
||||
"""가격 필터링을 적용하고 옵션을 조정"""
|
||||
|
|
@ -466,7 +494,7 @@ class OptionHandler:
|
|||
options_list = [
|
||||
{
|
||||
"name": name,
|
||||
"price": (info['low_price'] + info['high_price']) / 2 # 중간 값을 사용하여 필터링
|
||||
"price": self.round_to_UP((info['low_price'] + info['high_price']) / 2) # 중간 값을 사용하여 필터링
|
||||
}
|
||||
for name, info in prices.items()
|
||||
]
|
||||
|
|
@ -501,43 +529,41 @@ class OptionHandler:
|
|||
try:
|
||||
# 옵션 체크 상태를 수집한 정보에서 필터링된 옵션들만 체크 상태로 유지
|
||||
for i, name in enumerate(self.option_info['original_names'].values()):
|
||||
checkbox_selector = self.checkbox_selector_template.format(i+1)
|
||||
checkbox_selector = self.checkbox_selector_template.format(index=i+1)
|
||||
checkbox_element = await self.page.query_selector(checkbox_selector)
|
||||
is_checked = self.option_info['checked_states'].get(name, False)
|
||||
|
||||
# 디버깅 로그: 현재 옵션 이름과 필터링된 옵션 이름 확인
|
||||
self.logger.debug(f"옵션 이름: {name}, 필터링된 옵션에 포함 여부: {name in filtered_option_names}, 현재 체크 상태: {is_checked}")
|
||||
|
||||
if checkbox_element:
|
||||
# 필터링된 이름에 포함되는 경우
|
||||
if name in filtered_option_names:
|
||||
# 체크되어 있지 않으면 체크 수행
|
||||
if not is_checked:
|
||||
await checkbox_element.click()
|
||||
self.logger.debug(f"옵션 '{name}' 체크함")
|
||||
self.option_info['checked_states'][name] = True
|
||||
# 필터링된 이름에 포함되지 않는 경우
|
||||
else:
|
||||
# 체크되어 있으면 체크 해제 수행
|
||||
if is_checked:
|
||||
await checkbox_element.click()
|
||||
self.logger.debug(f"옵션 '{name}' 체크 해제함")
|
||||
self.option_info['checked_states'][name] = False
|
||||
|
||||
self.logger.debug(f"옵션 체크 상태 조정 완료.")
|
||||
except Exception as e:
|
||||
self.logger.error(f"옵션 체크 상태 조정 중 오류 발생: {e}", exc_info=True)
|
||||
|
||||
|
||||
async def AtoZ_button_click(self):
|
||||
AtoZ_button_locator = self.locator_manager.get_locator('OptionLocators', 'AtoZ_button_locator')
|
||||
self.logger.debug("A-Z 버튼을 클릭합니다.")
|
||||
await self.page.click(AtoZ_button_locator)
|
||||
await self.page.click(self.AtoZ_button_locator)
|
||||
|
||||
async def low_order_click(self):
|
||||
low_order_button_locator = self.locator_manager.get_locator('OptionLocators', 'low_order_button_locator')
|
||||
self.logger.debug("가격 낮은 순 정렬을 클릭합니다.")
|
||||
await self.page.click(low_order_button_locator)
|
||||
|
||||
async def save_option(self):
|
||||
"""옵션 수정 후 저장 버튼 클릭"""
|
||||
save_button_locator = self.locator_manager.get_locator('OptionLocators', 'save_button_locator')
|
||||
|
||||
try:
|
||||
await self.page.click(save_button_locator)
|
||||
self.logger.debug("옵션 수정 내용 저장 완료.")
|
||||
except Exception as e:
|
||||
self.logger.debug(f"옵션수정 후 저장 버튼 클릭 중 오류: {e}", exc_info=True)
|
||||
await self.page.click(self.low_order_button_locator)
|
||||
|
||||
async def update_option_image(self, index, option_image_url, product_name, option_name, debug_flag=False):
|
||||
"""
|
||||
|
|
@ -557,9 +583,9 @@ class OptionHandler:
|
|||
|
||||
self.logger.debug(f"{index}번째 옵션의 이미지를 업데이트합니다.")
|
||||
|
||||
delete_button_selector = self.delete_button_selector_template.format(index)
|
||||
delete_button_selector = self.delete_button_selector_template.format(index=index)
|
||||
confirm_delete_button_locator = self.confirm_delete_button_locator
|
||||
add_button_selector = self.add_button_selector_template.format(index)
|
||||
add_button_selector = self.add_button_selector_template.format(index=index)
|
||||
file_input_locator = self.file_input_locator
|
||||
|
||||
# 기존 이미지 삭제 (삭제 버튼이 존재할 경우)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,117 @@
|
|||
import re
|
||||
import jieba
|
||||
from collections import Counter
|
||||
from transformers import pipeline
|
||||
|
||||
class ProductOptionTranslator:
|
||||
def __init__(self, product_name, options, translator):
|
||||
"""
|
||||
옵션 명칭을 간소화하고 번역하는 클래스.
|
||||
|
||||
Parameters:
|
||||
- product_name (str): 상품명
|
||||
- options (list): 옵션명 리스트 (중국어 원문)
|
||||
- translator (object): 번역기 객체 (예: transformers의 pipeline 등)
|
||||
"""
|
||||
self.product_name = product_name
|
||||
self.options = options
|
||||
self.translator = translator
|
||||
|
||||
def simplify_options(self):
|
||||
"""
|
||||
각 옵션의 특징을 간결하게 남기고 불필요한 정보를 제거합니다.
|
||||
|
||||
Returns:
|
||||
- simplified_options (list): 간소화된 옵션명 리스트
|
||||
"""
|
||||
# 상품명에서 공통적인 키워드 추출
|
||||
common_words = self.extract_common_words()
|
||||
simplified_options = []
|
||||
|
||||
for option in self.options:
|
||||
# 형태소 분석을 통해 단어 단위로 분리
|
||||
words = list(jieba.cut(option))
|
||||
|
||||
# 의미 있는 단어만 남기기 (숫자, 영어, 특수한 패턴 유지)
|
||||
keywords = [word for word in words if self.is_significant(word, common_words)]
|
||||
|
||||
# 간결하게 옵션명 재구성
|
||||
simplified_option = " ".join(keywords)
|
||||
simplified_options.append(simplified_option)
|
||||
|
||||
return simplified_options
|
||||
|
||||
def extract_common_words(self):
|
||||
"""
|
||||
상품명에서 공통적으로 사용되는 키워드를 추출합니다.
|
||||
|
||||
Returns:
|
||||
- common_words (set): 상품명에서 중복되는 일반적인 단어들의 집합
|
||||
"""
|
||||
product_words = list(jieba.cut(self.product_name))
|
||||
word_counts = Counter(product_words)
|
||||
common_words = {word for word, count in word_counts.items() if count > 1}
|
||||
return common_words
|
||||
|
||||
def is_significant(self, word, common_words):
|
||||
"""
|
||||
단어가 의미 있는지 확인합니다. 의미 없는 수식어나 중복된 단어는 제거합니다.
|
||||
|
||||
Parameters:
|
||||
- word (str): 검사할 단어
|
||||
- common_words (set): 상품명에서 추출한 공통 단어들
|
||||
|
||||
Returns:
|
||||
- (bool): 단어가 의미 있는 경우 True, 그렇지 않은 경우 False
|
||||
"""
|
||||
# 공통 단어 또는 의미 없는 수식어는 제거
|
||||
if word in common_words:
|
||||
return False
|
||||
# 숫자, 영어 또는 특정 패턴(전압, 전류 등)은 의미 있는 단어로 간주
|
||||
if re.match(r'[A-Za-z0-9]+', word) or re.match(r'\d+(V|A|W|Hz)', word):
|
||||
return True
|
||||
return True if len(word) > 1 else False
|
||||
|
||||
def translate_options(self, simplified_options):
|
||||
"""
|
||||
간소화된 옵션명을 한글로 번역합니다.
|
||||
|
||||
Parameters:
|
||||
- simplified_options (list): 간소화된 옵션명 리스트
|
||||
|
||||
Returns:
|
||||
- translated_options (list): 한글로 번역된 옵션명 리스트 (25자 이내로 축약)
|
||||
"""
|
||||
translated_options = []
|
||||
for option in simplified_options:
|
||||
translated_text = self.translator(option, src_lang="zh", tgt_lang="ko")
|
||||
# 번역된 결과가 25자를 넘을 경우 축약
|
||||
if len(translated_text) > 25:
|
||||
translated_text = translated_text[:25] + '...'
|
||||
translated_options.append(translated_text)
|
||||
return translated_options
|
||||
|
||||
def process_options(self):
|
||||
"""
|
||||
전체 옵션 처리 과정을 수행합니다 (간소화 + 번역).
|
||||
|
||||
Returns:
|
||||
- translated_options (list): 최종 한글로 번역된 옵션 리스트
|
||||
"""
|
||||
simplified_options = self.simplify_options()
|
||||
return self.translate_options(simplified_options)
|
||||
|
||||
|
||||
# 번역기 예시 (transformers의 pipeline을 사용한 경우)
|
||||
translator = pipeline('translation', model='Helsinki-NLP/opus-mt-zh-ko')
|
||||
|
||||
# 예제 데이터
|
||||
product_name = "전기 드릴 18V 가정용"
|
||||
options = ["18V 2.0Ah 배터리 포함", "18V 본체만", "충전기 포함 세트", "전용 케이스"]
|
||||
|
||||
# 클래스 사용
|
||||
translator_class = ProductOptionTranslator(product_name, options, translator)
|
||||
translated_options = translator_class.process_options()
|
||||
|
||||
for idx, translated_option in enumerate(translated_options, 1):
|
||||
print(f"옵션 {idx}: {translated_option}")
|
||||
42
price.py
42
price.py
|
|
@ -574,8 +574,8 @@ class PriceHandler:
|
|||
|
||||
# 옵션 개수만큼 순회하면서 값을 수집
|
||||
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_locator = self.product_cost_locator_template.format(index=i+1)
|
||||
standard_selling_price_locator = self.standard_selling_price_locator_template.format(index=i+1)
|
||||
|
||||
# 각 선택자를 사용하여 요소 찾기
|
||||
product_cost_element = await self.page.wait_for_selector(product_cost_locator)
|
||||
|
|
@ -735,7 +735,7 @@ class PriceHandler:
|
|||
"""
|
||||
krw = cny * exchange_rate
|
||||
return krw
|
||||
|
||||
|
||||
def calculate_category_extra_shipping(self, category: str, product_price: int) -> int:
|
||||
"""
|
||||
카테고리와 상품가를 기반으로 추가 해외배송비를 계산합니다.
|
||||
|
|
@ -749,37 +749,21 @@ class PriceHandler:
|
|||
"""
|
||||
total_extra_shipping = 0
|
||||
self.logger.debug(f"category : {category}")
|
||||
cmb_config = self.cmb_diag.get_crmobi_stage(category)
|
||||
|
||||
# locator_manager에서 카테고리별 해외배송비 설정을 불러옴
|
||||
category_data = self.locator_manager.get_category_data(category)
|
||||
# cmb_diag에서 카테고리별 해외배송비 설정을 불러옴
|
||||
category_data = self.cmb_diag.get_crmobi_stage(category)
|
||||
|
||||
if category_data: # 카테고리 설정이 있는지 확인
|
||||
threshold_index = 1
|
||||
threshold, unit, extra_shipping = category_data
|
||||
|
||||
# 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 # 다음 구간으로 이동
|
||||
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} 추가 적용")
|
||||
|
||||
else:
|
||||
self.logger.debug(f"카테고리 {category}는 추가 해외배송비 규칙이 없습니다. 추가 해외배송비는 0으로 설정됩니다.")
|
||||
|
||||
self.logger.debug(f"총 추가 해외배송비: {total_extra_shipping}")
|
||||
return total_extra_shipping
|
||||
return total_extra_shipping
|
||||
|
|
@ -0,0 +1,82 @@
|
|||
from sqlalchemy import create_engine, text
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
from sqlalchemy.exc import SQLAlchemyError
|
||||
import os, shutil
|
||||
|
||||
class DatabaseManager:
|
||||
def __init__(self, db_url, logger=None):
|
||||
self.logger = logger
|
||||
self.db_url = db_url
|
||||
self.engine = create_engine(db_url, echo=False)
|
||||
self.Session = sessionmaker(bind=self.engine)
|
||||
|
||||
if self.logger:
|
||||
self.logger.debug(f"Database engine created with URL: {db_url}")
|
||||
|
||||
def get_session(self):
|
||||
"""DB 세션을 생성하고 반환"""
|
||||
if self.logger:
|
||||
self.logger.debug("Creating a new database session.")
|
||||
return self.Session()
|
||||
|
||||
def close_engine(self):
|
||||
"""DB 엔진을 종료"""
|
||||
if self.logger:
|
||||
self.logger.debug("Closing the database engine.")
|
||||
self.engine.dispose()
|
||||
|
||||
def create_db_file(self, db_path, initial_db_path):
|
||||
"""DB 파일이 없을 경우 초기 DB 파일을 복사하여 생성"""
|
||||
if not os.path.exists(db_path):
|
||||
if self.logger:
|
||||
self.logger.debug(f"Creating user DB file from initial DB: {initial_db_path} -> {db_path}")
|
||||
shutil.copyfile(initial_db_path, db_path)
|
||||
else:
|
||||
if self.logger:
|
||||
self.logger.debug(f"User DB file already exists at: {db_path}")
|
||||
|
||||
def execute_query(self, query, params=None):
|
||||
"""
|
||||
쿼리를 실행하고 결과를 반환하지 않음
|
||||
|
||||
Parameters:
|
||||
query (str): 실행할 쿼리 문자열
|
||||
params (dict, optional): 쿼리에 사용할 매개변수
|
||||
"""
|
||||
with self.get_session() as session:
|
||||
try:
|
||||
session.execute(text(query), params)
|
||||
session.commit()
|
||||
if self.logger:
|
||||
self.logger.debug(f"Executed query: {query} with params: {params}")
|
||||
except SQLAlchemyError as e:
|
||||
if self.logger:
|
||||
self.logger.error(f"Error executing query: {query}, params: {params}, error: {e}")
|
||||
session.rollback()
|
||||
raise
|
||||
|
||||
def fetchone(self, query, params=None):
|
||||
"""쿼리를 실행하고 단일 행을 반환"""
|
||||
with self.get_session() as session:
|
||||
try:
|
||||
result = session.execute(text(query), params).fetchone()
|
||||
if self.logger:
|
||||
self.logger.debug(f"Fetched one result for query: {query} with params: {params}, result: {result}")
|
||||
return result
|
||||
except SQLAlchemyError as e:
|
||||
if self.logger:
|
||||
self.logger.error(f"Error fetching one result: {query}, params: {params}, error: {e}")
|
||||
raise
|
||||
|
||||
def fetchall(self, query, params=None):
|
||||
"""쿼리를 실행하고 모든 행을 반환"""
|
||||
with self.get_session() as session:
|
||||
try:
|
||||
result = session.execute(text(query), params).fetchall()
|
||||
if self.logger:
|
||||
self.logger.debug(f"Fetched all results for query: {query} with params: {params}, result count: {len(result)}")
|
||||
return result
|
||||
except SQLAlchemyError as e:
|
||||
if self.logger:
|
||||
self.logger.error(f"Error fetching all results: {query}, params: {params}, error: {e}")
|
||||
raise
|
||||
851
src/cmb_diag.py
851
src/cmb_diag.py
File diff suppressed because it is too large
Load Diff
Binary file not shown.
Loading…
Reference in New Issue