This commit is contained in:
Envy_PC 2024-10-14 21:05:42 +09:00
parent 6be03a3c0b
commit d8a7aecf7f
20 changed files with 7339 additions and 434 deletions

26
Todo.List Normal file
View File

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

File diff suppressed because it is too large Load Diff

View File

@ -170,7 +170,7 @@ class BrowserController:
}}''', self.total_product_count_locator) }}''', self.total_product_count_locator)
if element_text: if element_text:
self.logger.debug(f"가져온 텍스트: {element_text}") # 텍스트 확인용 로그 self.logger.debug(f"총 상품수 확인: {element_text}") # 텍스트 확인용 로그
# "총 xx개 상품"에서 숫자만 추출 # "총 xx개 상품"에서 숫자만 추출
count = int(''.join(filter(str.isdigit, element_text))) count = int(''.join(filter(str.isdigit, element_text)))
return count return count

View File

@ -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' 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' 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' 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' 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[{i}]/td[4]/div/div/div[1]/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] [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_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[{i}]/div/div[1]/div/div[3]/div[2]/div[1]/span/input' 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(., '단일 상품등록')]' 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(., '옵션 상품등록')]' option_product_locator = '//div[@id="productMainContentContainerId"]//label[contains(@class, 'ant-radio-button-wrapper-checked') and contains(., '옵션 상품등록')]'
total_options_selector = '#productMainContentContainerId label.ant-checkbox-wrapper' total_options_selector = '#productMainContentContainerId label.ant-checkbox-wrapper'
is_all_option_checked_selector = '#productMainContentContainerId .ant-checkbox-indeterminate' 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' 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({i}) > div > div:nth-child(1) > div > div:nth-child(3) > div:nth-child(2) > div:nth-child(1) > span > input' 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({i}) input[type="checkbox"]' checkbox_selector_template = '#productMainContentContainerId li:nth-child({index}) input[type="checkbox"]'
image_selector_template = '#productMainContentContainerId li:nth-child({i}) img.sc-gbvfcU.ezktkd' image_selector_template = '#productMainContentContainerId li:nth-child({index}) img.sc-gbvfcU.ezktkd'
price_selector_template = '#productMainContentContainerId li:nth-child({i}) sup' 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({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' 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' 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"]' file_input_locator = 'input[type="file"]'
low_order_button_locator = 'button:has-text("가격 낮은 순")'
AtoZ_button_locator = 'button:has-text("A-Z")'
[DetailLocators] [DetailLocators]
product_detail_input_locator = '//*[@id='detailMainContainerId']/div/div/div[{i}]/textarea' product_detail_input_locator = '//*[@id='detailMainContainerId']/div/div/div[{i}]/textarea'

24
gui.py
View File

@ -10,9 +10,11 @@ from price import PriceHandler
from title import TitleHandler from title import TitleHandler
from locatorManager import LocatorManager from locatorManager import LocatorManager
from src.cmb_diag import CMBSettingsDialog from src.cmb_diag import CMBSettingsDialog
from src.DatabaseManager import DatabaseManager
from logger_module import QTextEditLogger # 추가 from logger_module import QTextEditLogger # 추가
import logging import logging
import asyncio import asyncio
import os, shutil
class TranslationApp(QWidget): class TranslationApp(QWidget):
def __init__(self, logger=None, app=None): def __init__(self, logger=None, app=None):
@ -32,7 +34,23 @@ class TranslationApp(QWidget):
self.vertexAI = VertexAITranslator(self.logger, key_path) self.vertexAI = VertexAITranslator(self.logger, key_path)
self.optionHandler = None 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.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.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) 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) self.complete_stage(0)
if self.toggle_states['optionTrnas'] or self.toggle_states['optionIMGTrans'] or self.toggle_states['optionAutoSelect']: 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) self.start_stage(0)
await self.edit_option(product_name) await self.edit_option(product_name)
@ -811,7 +829,7 @@ class TranslationApp(QWidget):
# 옵션 최대선택갯수 # 옵션 최대선택갯수
max_option_count = 20 max_option_count = 20
option_image_trans = False option_image_trans = False
await self.optionHandler.process_options(product_name, max_option_count, option_image_trans) await self.optionHandler.process_options(product_name, max_option_count, self.toggle_states)
# 수정 후 저장 # 수정 후 저장
# await self.optionHandler.save_option() # await self.optionHandler.save_option()

View File

@ -73,6 +73,8 @@ class LocatorManager:
'confirm_delete_button_locator': self.config.get('OptionLocators', 'confirm_delete_button_locator').strip("'"), '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("'"), 'add_button_selector_template': self.config.get('OptionLocators', 'add_button_selector_template').strip("'"),
'file_input_locator': self.config.get('OptionLocators', 'file_input_locator').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 섹션 # TitleLocators 섹션

130
option.py
View File

@ -2,7 +2,7 @@ from collections import Counter
import pyautogui import pyautogui
from datetime import datetime from datetime import datetime
import numpy as np import numpy as np
import asyncio import asyncio, time, math
class OptionHandler: class OptionHandler:
def __init__(self, locator_manager, browser_controller, whale_translator, logger, vertexAI, debug_flag=False): 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.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.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.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): def update_page(self, page1):
self.page = page1 self.page = page1
@ -81,7 +83,12 @@ class OptionHandler:
std_price = np.std(prices) std_price = np.std(prices)
self.logger.debug(f"최저옵션: {mean_price}, 표준편차: {std_price}") self.logger.debug(f"최저옵션: {mean_price}, 표준편차: {std_price}")
if std_price == 0:
# 모든 옵션의 가격이 동일한 경우 그대로 반환
self.logger.debug("모든 옵션의 가격이 동일합니다. 필터링 없이 모든 옵션을 반환합니다.")
return options
filtered_options = [] filtered_options = []
for option in options: for option in options:
z_score = (option['price'] - mean_price) / std_price 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] 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 return final_options
async def store_selected_options(self): async def store_selected_options(self):
@ -111,13 +117,13 @@ class OptionHandler:
try: try:
selected_translated_options = [] 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() await self.low_order_click()
for i in range(1, total_options_count + 1): for i in range(1, total_options_count + 1):
option_excluded_selector = self.option_excluded_selector_template.format(i) option_excluded_selector = self.option_excluded_selector_template.format(index=i)
option_input_selector = self.option_input_selector_template.format(i) option_input_selector = self.option_input_selector_template.format(index=i)
option_excluded_element = await self.page.query_selector(option_excluded_selector) option_excluded_element = await self.page.query_selector(option_excluded_selector)
if not option_excluded_element: if not option_excluded_element:
@ -135,7 +141,7 @@ class OptionHandler:
self.logger.error(f"선택된 옵션 저장 중 오류 발생: {e}", exc_info=True) 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() await self.low_order_click()
# 4. 옵션 정보 수집 및 번역 # 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: try:
# Vertex AI를 통한 번역 시도 # Vertex AI를 통한 번역 시도
translated_options = await self.vertexAItranslator.translate_options(self.option_info['original_names'], product_name) translated_options = await self.vertexAItranslator.translate_options(self.option_info['original_names'], product_name)
self.logger.debug(f"번역된 옵션 입력") self.logger.debug(f"번역된 옵션 입력")
await self.apply_translated_options(translated_options, self.option_info['edit_fields']) await self.apply_translated_options(translated_options, self.option_info['edit_fields'])
translation_success = True # 번역 성공 translation_success = True # 번역 성공
except ValueError as ve: except ValueError as ve:
if "SAFETY" in str(ve): if "SAFETY" in str(ve):
self.logger.error(f"안전 필터에 의해 번역 요청이 차단되었습니다. {ve}") self.logger.error(f"안전 필터에 의해 번역 요청이 차단되었습니다. {ve}")
self.logger.debug(f"퍼센티 자체 AI번역 사용 시도") self.logger.debug(f"퍼센티 자체 AI번역 사용 시도")
pyautogui.hotkey('alt', 'q') pyautogui.hotkey('alt', 'q')
translation_success = False # 번역 실패 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. 옵션 필터링 및 조정 # 5. 옵션 필터링 및 조정
self.logger.debug(f"옵션 필터링 및 조정") if toggle_states['optionAutoSelect']:
await self.filter_and_adjust_options(max_option_count) self.logger.debug(f"옵션 필터링 및 조정 : {toggle_states['optionAutoSelect']}")
await self.filter_and_adjust_options(max_option_count)
# 6. 선택된 옵션 재수집 # 6. 선택된 옵션 재수집
self.logger.debug(f"옵션 필터링 및 조정") self.logger.debug(f"옵션 필터링 및 조정")
await self.store_selected_options() # 페이지에서 실제 선택된 옵션을 수집하여 저장 await self.store_selected_options() # 페이지에서 실제 선택된 옵션을 수집하여 저장
# 7. 옵션 이미지 업데이트 (옵션 이미지가 있는 경우) # 7. 옵션 이미지 업데이트 (옵션 이미지가 있는 경우)
if option_image_trans_flag: if toggle_states['optionIMGTrans']:
self.logger.debug("옵션 이미지 업데이트 (옵션 이미지가 있는 경우만)") self.logger.debug(f"옵션 이미지번역(옵션 이미지가 있는 경우만) : {toggle_states['optionIMGTrans']}")
for index, option_image_url in enumerate(self.option_info.get('option_images', []), start=1): 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}') 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) 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): for i in range(1, total_options_count + 1):
try: try:
original_name_selector = self.original_name_selector_template.format(i) original_name_selector = self.original_name_selector_template.format(index=i)
edit_field_selector = self.edit_field_selector_template.format(i) edit_field_selector = self.edit_field_selector_template.format(index=i)
checkbox_selector = self.checkbox_selector_template.format(i) checkbox_selector = self.checkbox_selector_template.format(index=i)
image_selector = self.image_selector_template.format(i) image_selector = self.image_selector_template.format(index=i)
price_selector = self.price_selector_template.format(i) price_selector = self.price_selector_template.format(index=i)
tasks = [ tasks = [
self.page.query_selector(original_name_selector), self.page.query_selector(original_name_selector),
@ -435,15 +449,15 @@ class OptionHandler:
if edit_field: if edit_field:
await edit_field.fill(translated_name) # 필드에 번역된 옵션명 입력 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 updated_translations[original_name] = translated_name
else: else:
self.logger.debug(f"{key}번째 옵션 필드가 없습니다.") self.logger.error(f"{key}번째 옵션 필드가 없습니다.")
else: else:
self.logger.debug(f"원본 옵션명을 찾을 수 없습니다: {origin_option_key}") self.logger.error(f"원본 옵션명을 찾을 수 없습니다: {origin_option_key}")
# 모든 번역이 끝난 후 한 번에 업데이트 # 모든 번역이 끝난 후 한 번에 업데이트
self.option_info['selected_translated_options'].update(updated_translations) self.option_info['selected_translated_options'].update(updated_translations)
@ -452,6 +466,20 @@ class OptionHandler:
except Exception as e: except Exception as e:
self.logger.error(f"번역된 옵션명을 입력하는 중 오류 발생: {e}", exc_info=True) 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): async def filter_and_adjust_options(self, max_option_count):
"""가격 필터링을 적용하고 옵션을 조정""" """가격 필터링을 적용하고 옵션을 조정"""
@ -466,7 +494,7 @@ class OptionHandler:
options_list = [ options_list = [
{ {
"name": name, "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() for name, info in prices.items()
] ]
@ -501,43 +529,41 @@ class OptionHandler:
try: try:
# 옵션 체크 상태를 수집한 정보에서 필터링된 옵션들만 체크 상태로 유지 # 옵션 체크 상태를 수집한 정보에서 필터링된 옵션들만 체크 상태로 유지
for i, name in enumerate(self.option_info['original_names'].values()): 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) checkbox_element = await self.page.query_selector(checkbox_selector)
is_checked = self.option_info['checked_states'].get(name, False) 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 checkbox_element:
# 필터링된 이름에 포함되는 경우
if name in filtered_option_names: if name in filtered_option_names:
# 체크되어 있지 않으면 체크 수행
if not is_checked: if not is_checked:
await checkbox_element.click() await checkbox_element.click()
self.logger.debug(f"옵션 '{name}' 체크함")
self.option_info['checked_states'][name] = True self.option_info['checked_states'][name] = True
# 필터링된 이름에 포함되지 않는 경우
else: else:
# 체크되어 있으면 체크 해제 수행
if is_checked: if is_checked:
await checkbox_element.click() await checkbox_element.click()
self.logger.debug(f"옵션 '{name}' 체크 해제함")
self.option_info['checked_states'][name] = False self.option_info['checked_states'][name] = False
self.logger.debug(f"옵션 체크 상태 조정 완료.") self.logger.debug(f"옵션 체크 상태 조정 완료.")
except Exception as e: except Exception as e:
self.logger.error(f"옵션 체크 상태 조정 중 오류 발생: {e}", exc_info=True) self.logger.error(f"옵션 체크 상태 조정 중 오류 발생: {e}", exc_info=True)
async def AtoZ_button_click(self): async def AtoZ_button_click(self):
AtoZ_button_locator = self.locator_manager.get_locator('OptionLocators', 'AtoZ_button_locator')
self.logger.debug("A-Z 버튼을 클릭합니다.") 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): async def low_order_click(self):
low_order_button_locator = self.locator_manager.get_locator('OptionLocators', 'low_order_button_locator')
self.logger.debug("가격 낮은 순 정렬을 클릭합니다.") self.logger.debug("가격 낮은 순 정렬을 클릭합니다.")
await self.page.click(low_order_button_locator) await self.page.click(self.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)
async def update_option_image(self, index, option_image_url, product_name, option_name, debug_flag=False): 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}번째 옵션의 이미지를 업데이트합니다.") 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 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 file_input_locator = self.file_input_locator
# 기존 이미지 삭제 (삭제 버튼이 존재할 경우) # 기존 이미지 삭제 (삭제 버튼이 존재할 경우)

117
option_translator.py Normal file
View File

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

View File

@ -574,8 +574,8 @@ class PriceHandler:
# 옵션 개수만큼 순회하면서 값을 수집 # 옵션 개수만큼 순회하면서 값을 수집
for i in range(1, total_options + 1): for i in range(1, total_options + 1):
product_cost_locator = self.product_cost_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(i=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) product_cost_element = await self.page.wait_for_selector(product_cost_locator)
@ -735,7 +735,7 @@ class PriceHandler:
""" """
krw = cny * exchange_rate krw = cny * exchange_rate
return krw return krw
def calculate_category_extra_shipping(self, category: str, product_price: int) -> int: def calculate_category_extra_shipping(self, category: str, product_price: int) -> int:
""" """
카테고리와 상품가를 기반으로 추가 해외배송비를 계산합니다. 카테고리와 상품가를 기반으로 추가 해외배송비를 계산합니다.
@ -749,37 +749,21 @@ class PriceHandler:
""" """
total_extra_shipping = 0 total_extra_shipping = 0
self.logger.debug(f"category : {category}") self.logger.debug(f"category : {category}")
cmb_config = self.cmb_diag.get_crmobi_stage(category)
# locator_manager에서 카테고리별 해외배송비 설정을 불러옴 # cmb_diag에서 카테고리별 해외배송비 설정을 불러옴
category_data = self.locator_manager.get_category_data(category) category_data = self.cmb_diag.get_crmobi_stage(category)
if category_data: # 카테고리 설정이 있는지 확인 if category_data: # 카테고리 설정이 있는지 확인
threshold_index = 1 threshold, unit, extra_shipping = category_data
# threshold와 extra_shipping의 dynamic한 처리 if product_price > threshold:
while True: excess_amount = product_price - threshold
threshold_key = f'threshold_{threshold_index}' increments = excess_amount // unit
extra_shipping_key = f'extra_shipping_{threshold_index}' total_extra_shipping += increments * extra_shipping
unit_key = f'unit_{threshold_index}' self.logger.debug(f"{category} 카테고리, threshold {threshold}을 초과하여 추가 해외배송비 {increments * extra_shipping} 추가 적용")
# 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: else:
self.logger.debug(f"카테고리 {category}는 추가 해외배송비 규칙이 없습니다. 추가 해외배송비는 0으로 설정됩니다.") self.logger.debug(f"카테고리 {category}는 추가 해외배송비 규칙이 없습니다. 추가 해외배송비는 0으로 설정됩니다.")
self.logger.debug(f"총 추가 해외배송비: {total_extra_shipping}") self.logger.debug(f"총 추가 해외배송비: {total_extra_shipping}")
return total_extra_shipping return total_extra_shipping

82
src/DatabaseManager.py Normal file
View File

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

File diff suppressed because it is too large Load Diff

Binary file not shown.