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

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'
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
View File

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

View File

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

@ -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
# 기존 이미지 삭제 (삭제 버튼이 존재할 경우)

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):
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

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.