크무비 조정

This commit is contained in:
R5600U_PC 2024-10-12 23:17:42 +09:00
parent 0bf7a57acb
commit ae42321b4f
19 changed files with 11551 additions and 258 deletions

Binary file not shown.

Binary file not shown.

Binary file not shown.

File diff suppressed because it is too large Load Diff

View File

@ -12,7 +12,7 @@ class BrowserController:
self.app = app self.app = app
self.logger = logger self.logger = logger
self.locator_manager = locator_manager self.locator_manager = locator_manager
self.chrome_window_name = "퍼센티 - 셀러들을 위한 AI 구매대행 솔루션 - Chrome" # self.chrome_window_name = "퍼센티 - 셀러들을 위한 AI 구매대행 솔루션 - Chrome"
# self.whale_window_name = "새 시크릿 탭 - Whale" # self.whale_window_name = "새 시크릿 탭 - Whale"
self.chrome_hwnd = None self.chrome_hwnd = None
self.whale_hwnd = None self.whale_hwnd = None
@ -22,6 +22,7 @@ class BrowserController:
self.page = None self.page = None
# BrowserController에 해당하는 모든 locator를 정의 # BrowserController에 해당하는 모든 locator를 정의
self.chrome_window_name = self.locator_manager.get_locator('BrowserControl', 'chrome_window_name')
self.login_email_locator = self.locator_manager.get_locator('BrowserControl', 'login_email_locator') self.login_email_locator = self.locator_manager.get_locator('BrowserControl', 'login_email_locator')
self.login_password_locator = self.locator_manager.get_locator('BrowserControl', 'login_password_locator') self.login_password_locator = self.locator_manager.get_locator('BrowserControl', 'login_password_locator')
self.login_button_locator = self.locator_manager.get_locator('BrowserControl', 'login_button_locator') self.login_button_locator = self.locator_manager.get_locator('BrowserControl', 'login_button_locator')
@ -30,6 +31,7 @@ class BrowserController:
self.staff_login_button_locator = self.locator_manager.get_locator('BrowserControl', 'staff_login_button_locator') self.staff_login_button_locator = self.locator_manager.get_locator('BrowserControl', 'staff_login_button_locator')
self.close_ad_dialog_locator = self.locator_manager.get_locator('BrowserControl', 'close_ad_dialog_locator') self.close_ad_dialog_locator = self.locator_manager.get_locator('BrowserControl', 'close_ad_dialog_locator')
self.close_ad_button_locator = self.locator_manager.get_locator('BrowserControl', 'close_ad_button_locator') self.close_ad_button_locator = self.locator_manager.get_locator('BrowserControl', 'close_ad_button_locator')
self.total_product_count_locator = self.locator_manager.get_locator('BrowserControl', 'total_product_count_locator')
self.product_name_template = self.locator_manager.get_locator('BrowserControl', 'product_name_template') self.product_name_template = self.locator_manager.get_locator('BrowserControl', 'product_name_template')
self.product_price_template = self.locator_manager.get_locator('BrowserControl', 'product_price_template') self.product_price_template = self.locator_manager.get_locator('BrowserControl', 'product_price_template')
self.product_image_template = self.locator_manager.get_locator('BrowserControl', 'product_image_template') self.product_image_template = self.locator_manager.get_locator('BrowserControl', 'product_image_template')
@ -42,8 +44,14 @@ class BrowserController:
self.ck_source_editing_area_locator = self.locator_manager.get_locator('BrowserControl', 'ck_source_editing_area_locator') self.ck_source_editing_area_locator = self.locator_manager.get_locator('BrowserControl', 'ck_source_editing_area_locator')
self.option_input_field_locator = self.locator_manager.get_locator('BrowserControl', 'option_input_field_locator') self.option_input_field_locator = self.locator_manager.get_locator('BrowserControl', 'option_input_field_locator')
self.text_templates = self.locator_manager.selectors.get('DetailPageTextTemplates', {}) self.text_templates = self.locator_manager.selectors.get('DetailPageTextTemplates', {})
self.product_name_template_xpath = self.locator_manager.get_locator('BrowserControl', 'product_name_template_xpath') self.title_tab_locator = self.locator_manager.get_locator('BrowserControl', 'title_tab_locator')
self.option_tab_locator = self.locator_manager.get_locator('BrowserControl', 'option_tab_locator')
self.price_tab_locator = self.locator_manager.get_locator('BrowserControl', 'price_tab_locator')
self.tag_tab_locator = self.locator_manager.get_locator('BrowserControl', 'tag_tab_locator')
self.thumb_tab_locator = self.locator_manager.get_locator('BrowserControl', 'thumb_tab_locator')
self.detail_tab_locator = self.locator_manager.get_locator('BrowserControl', 'detail_tab_locator')
self.upload_tab_locator = self.locator_manager.get_locator('BrowserControl', 'upload_tab_locator')
self.save_button_locator = self.locator_manager.get_locator('BrowserControl', 'save_button_locator')
def get_page(self): def get_page(self):
return self.page return self.page
@ -133,7 +141,7 @@ class BrowserController:
self.logger.debug('크롬 창을 찾을 수 없습니다.') self.logger.debug('크롬 창을 찾을 수 없습니다.')
async def get_total_product_count(self): async def get_total_product_count_ori(self):
try: try:
# JavaScript로 해당 요소의 텍스트를 가져옴 # JavaScript로 해당 요소의 텍스트를 가져옴
element_text = await self.page.evaluate('''() => { element_text = await self.page.evaluate('''() => {
@ -153,38 +161,58 @@ class BrowserController:
self.logger.debug(f"상품 수를 가져오는 중 오류 발생: {e}", exc_info=True) self.logger.debug(f"상품 수를 가져오는 중 오류 발생: {e}", exc_info=True)
return 0 return 0
async def get_total_product_count(self):
async def get_product_name(self, index, selector='xpath'):
"""
상품명을 수집하는 메서드
index : 상품명을 수집하는 인덱스
selector : 수집방법 (css 또는 xpath)
"""
try: try:
# config.ini에서 설정된 선택자에 인덱스를 적용하여 가져옴 # Python 변수를 JavaScript로 전달하여 요소의 텍스트를 가져옴
# product_name_selector = self.product_name_template.format(index=index) element_text = await self.page.evaluate(f'''(selector) => {{
# self.logger.debug(f"사용된 선택자: {product_name_selector}") # 선택자 출력 let element = document.querySelector(selector);
return element ? element.innerText : null;
}}''', self.total_product_count_locator)
product_name_xpath_selector = self.product_name_template_xpath.format(index=index) if element_text:
self.logger.debug(f"사용된 선택자: {product_name_xpath_selector}") # 선택자 출력 self.logger.debug(f"가져온 텍스트: {element_text}") # 텍스트 확인용 로그
# "총 xx개 상품"에서 숫자만 추출
count = int(''.join(filter(str.isdigit, element_text)))
# XPath 기반으로 요소 검색 return count
product_name_element = await self.page.locator(f"xpath={product_name_xpath_selector}").element_handle() else:
self.logger.debug("요소를 찾을 수 없습니다.")
# product_name_element = await self.page.query_selector(product_name_selector) return 0
# product_name_element가 None인지 확인
if product_name_element is None:
self.logger.error(f"상품명 요소를 찾을 수 없습니다: index {index}")
return "수집 오류 발생"
# 요소가 존재할 경우, inner_text 수집
product_name = await product_name_element.inner_text()
return product_name.strip()
except Exception as e: except Exception as e:
self.logger.error(f"상품명 수집 중 오류: {e}") self.logger.debug(f"상품 수를 가져오는 중 오류 발생: {e}", exc_info=True)
return "수집 오류 발생" return 0
# async def get_product_name(self, index, selector='xpath'):
# """
# 상품명을 수집하는 메서드
# index : 상품명을 수집하는 인덱스
# selector : 수집방법 (css 또는 xpath)
# """
# try:
# # config.ini에서 설정된 선택자에 인덱스를 적용하여 가져옴
# # product_name_selector = self.product_name_template.format(index=index)
# # self.logger.debug(f"사용된 선택자: {product_name_selector}") # 선택자 출력
# product_name_xpath_selector = self.product_name_template_xpath.format(index=index)
# self.logger.debug(f"사용된 선택자: {product_name_xpath_selector}") # 선택자 출력
# # XPath 기반으로 요소 검색
# product_name_element = await self.page.locator(f"xpath={product_name_xpath_selector}").element_handle()
# # product_name_element = await self.page.query_selector(product_name_selector)
# # product_name_element가 None인지 확인
# if product_name_element is None:
# self.logger.error(f"상품명 요소를 찾을 수 없습니다: index {index}")
# return "수집 오류 발생"
# # 요소가 존재할 경우, inner_text 수집
# product_name = await product_name_element.inner_text()
# return product_name.strip()
# except Exception as e:
# self.logger.error(f"상품명 수집 중 오류: {e}")
# return "수집 오류 발생"
@ -287,7 +315,7 @@ class BrowserController:
self.logger.debug("세부사항 수정 및 업로드 버튼을 찾을 수 없습니다.") self.logger.debug("세부사항 수정 및 업로드 버튼을 찾을 수 없습니다.")
return [] return []
self.logger.debug(f"수정할 상품 개수: {button_count}") self.logger.debug(f"현재 페이지의 수정할 상품 개수: {button_count}")
# 모든 버튼을 리스트로 반환 # 모든 버튼을 리스트로 반환
return [buttons.nth(i) for i in range(button_count)] return [buttons.nth(i) for i in range(button_count)]
@ -333,30 +361,35 @@ class BrowserController:
async def click_detail_tab(self): async def click_detail_tab(self):
"""상세페이지 탭 클릭""" """상세페이지 탭 클릭"""
try: try:
detail_tab_locator = self.locator_manager.get_locator('BrowserControl', 'detail_tab_locator') await self.page.click(self.detail_tab_locator)
await self.page.click(detail_tab_locator)
self.logger.debug("상세페이지 탭 클릭 완료.") self.logger.debug("상세페이지 탭 클릭 완료.")
except Exception as e: except Exception as e:
self.logger.debug(f"상세페이지 탭 클릭 중 오류: {e}", exc_info=True) self.logger.debug(f"상세페이지 탭 클릭 중 오류: {e}", exc_info=True)
async def click_option_tab(self): async def click_option_tab(self):
"""상세페이지 탭 클릭""" """옵션 탭 클릭"""
try: try:
option_tab_locator = self.locator_manager.get_locator('BrowserControl', 'option_tab_locator') await self.page.click(self.option_tab_locator)
await self.page.click(option_tab_locator)
self.logger.debug("옵션 탭 클릭 완료.") self.logger.debug("옵션 탭 클릭 완료.")
except Exception as e: except Exception as e:
self.logger.debug(f"옵션 탭 클릭 중 오류: {e}", exc_info=True) self.logger.debug(f"옵션 탭 클릭 중 오류: {e}", exc_info=True)
async def click_price_tab(self): async def click_price_tab(self):
"""상세페이지 탭 클릭""" """가격 탭 클릭"""
try: try:
price_tab_locator = self.locator_manager.get_locator('BrowserControl', 'price_tab_locator') await self.page.click(self.price_tab_locator)
await self.page.click(price_tab_locator)
self.logger.debug("가격 탭 클릭 완료.") self.logger.debug("가격 탭 클릭 완료.")
except Exception as e: except Exception as e:
self.logger.debug(f"가격 탭 클릭 중 오류: {e}", exc_info=True) self.logger.debug(f"가격 탭 클릭 중 오류: {e}", exc_info=True)
async def click_title_tab(self):
"""상품명 탭 클릭"""
try:
await self.page.click(self.title_tab_locator)
self.logger.debug("상품명 탭 클릭 완료.")
except Exception as e:
self.logger.debug(f"상품명 탭 클릭 중 오류: {e}", exc_info=True)
async def extract_image_urls(self, optionHandler, is_option_data=False): async def extract_image_urls(self, optionHandler, is_option_data=False):
"""상세페이지에서 이미지 URL 추출""" """상세페이지에서 이미지 URL 추출"""
try: try:
@ -462,8 +495,7 @@ class BrowserController:
async def save_and_ecs_product_edit(self): async def save_and_ecs_product_edit(self):
"""상품 수정 후 저장 버튼 클릭""" """상품 수정 후 저장 버튼 클릭"""
try: try:
save_button_locator = self.locator_manager.get_locator('BrowserControl', 'save_button_locator') await self.page.click(self.save_button_locator)
await self.page.click(save_button_locator)
await self.page.keyboard.press("Escape") await self.page.keyboard.press("Escape")
self.logger.debug("상품 수정 내용 저장 및 ECS 완료.") self.logger.debug("상품 수정 내용 저장 및 ECS 완료.")
except Exception as e: except Exception as e:
@ -472,8 +504,7 @@ class BrowserController:
async def save_product_edit(self): async def save_product_edit(self):
"""상품 수정 후 저장 버튼 클릭""" """상품 수정 후 저장 버튼 클릭"""
try: try:
save_button_locator = self.locator_manager.get_locator('BrowserControl', 'save_button_locator') await self.page.click(self.save_button_locator)
await self.page.click(save_button_locator)
self.logger.debug("상품 수정 내용 저장 완료.") self.logger.debug("상품 수정 내용 저장 완료.")
except Exception as e: except Exception as e:
self.logger.debug(f"저장 버튼 클릭 중 오류: {e}", exc_info=True) self.logger.debug(f"저장 버튼 클릭 중 오류: {e}", exc_info=True)
@ -482,8 +513,7 @@ class BrowserController:
"""다음 페이지로 이동""" """다음 페이지로 이동"""
try: try:
# 현재 페이지가 몇 번째 페이지인지 확인 (클래스에 'ant-pagination-item-active'가 있는 요소) # 현재 페이지가 몇 번째 페이지인지 확인 (클래스에 'ant-pagination-item-active'가 있는 요소)
current_page_locator = self.locator_manager.get_locator('BrowserControl', 'current_page_locator') current_page = await self.page.query_selector(self.current_page_locator)
current_page = await self.page.query_selector(current_page_locator)
if not current_page: if not current_page:
self.logger.debug("현재 페이지 정보를 찾을 수 없습니다.") self.logger.debug("현재 페이지 정보를 찾을 수 없습니다.")
@ -494,8 +524,7 @@ class BrowserController:
next_page_number = current_page_number + 1 next_page_number = current_page_number + 1
# 다음 페이지 버튼을 찾음 (title 속성으로 다음 페이지를 찾음) # 다음 페이지 버튼을 찾음 (title 속성으로 다음 페이지를 찾음)
next_page_button_template = self.locator_manager.get_locator('BrowserControl', 'next_page_button_template') next_page_button_locator = self.next_page_button_template.format(page_number=next_page_number)
next_page_button_locator = next_page_button_template.format(page_number=next_page_number)
next_page_button = await self.page.query_selector(next_page_button_locator) next_page_button = await self.page.query_selector(next_page_button_locator)
if next_page_button: if next_page_button:
@ -606,43 +635,43 @@ class BrowserController:
self.logger.debug("최대 스크롤 횟수에 도달했습니다.") self.logger.debug("최대 스크롤 횟수에 도달했습니다.")
async def collect_product_info(self): # async def collect_product_info(self):
""" # """
상품 정보를 수집하는 메서드 # 상품 정보를 수집하는 메서드
""" # """
try: # try:
# 페이지를 아래로 스크롤하여 모든 상품 로드 # # 페이지를 아래로 스크롤하여 모든 상품 로드
await self.scroll_with_wheel('down') # await self.scroll_with_wheel('down')
await self.scroll_with_wheel('up') # await self.scroll_with_wheel('up')
product_infos = [] # product_infos = []
for i in range(1, 51): # 1부터 최대 50까지 상품 처리 # for i in range(1, 51): # 1부터 최대 50까지 상품 처리
try: # try:
# 각 상품의 CSS 선택자를 동적으로 생성하여 접근 # # 각 상품의 CSS 선택자를 동적으로 생성하여 접근
product_name_locator = self.product_name_template.format(i=i) # product_name_locator = self.product_name_template.format(i=i)
product_price_locator = self.product_price_template.format(i=i) # product_price_locator = self.product_price_template.format(i=i)
product_image_locator = self.product_image_template.format(i=i) # product_image_locator = self.product_image_template.format(i=i)
product_name_element = await self.page.query_selector(product_name_locator) # product_name_element = await self.page.query_selector(product_name_locator)
product_price_element = await self.page.query_selector(product_price_locator) # product_price_element = await self.page.query_selector(product_price_locator)
product_image_element = await self.page.query_selector(product_image_locator) # product_image_element = await self.page.query_selector(product_image_locator)
if product_name_element and product_price_element and product_image_element: # if product_name_element and product_price_element and product_image_element:
product_info = { # product_info = {
"name": await product_name_element.text_content().strip(), # "name": await product_name_element.text_content().strip(),
"price": await product_price_element.text_content().strip(), # "price": await product_price_element.text_content().strip(),
"image_url": await product_image_element.get_attribute('src') # "image_url": await product_image_element.get_attribute('src')
} # }
self.logger.debug(f"상품 {i}: {product_info}") # self.logger.debug(f"상품 {i}: {product_info}")
product_infos.append(product_info) # product_infos.append(product_info)
except Exception as e: # except Exception as e:
self.logger.error(f"상품 {i} 정보 수집 중 오류 발생: {e}", exc_info=True) # self.logger.error(f"상품 {i} 정보 수집 중 오류 발생: {e}", exc_info=True)
continue # continue
return product_infos # return product_infos
except Exception as e: # except Exception as e:
self.logger.error(f"상품 정보 수집 중 오류 발생: {e}", exc_info=True) # self.logger.error(f"상품 정보 수집 중 오류 발생: {e}", exc_info=True)
return [] # return []
async def scroll_page_to_bottom(self, pause_time=0.2): async def scroll_page_to_bottom(self, pause_time=0.2):

View File

@ -1,19 +1,19 @@
[PriceLocators] [PriceLocators]
return_fee_input_locator = //*[@id='productMainContentContainerId']/div/div[1]/div/div/div[4]/div/div[1]/div[3]/div/div/div/div[1]/div[2]/input return_fee_input_locator = '//*[@id='productMainContentContainerId']/div/div[1]/div/div/div[4]/div/div[1]/div[3]/div/div/div/div[1]/div[2]/input'
first_delv_fee_input_locator = //*[@id='productMainContentContainerId']/div/div[1]/div/div/div[4]/div/div[1]/div[4]/div/div[2]/div/div[1]/div[2]/input first_delv_fee_input_locator = '//*[@id='productMainContentContainerId']/div/div[1]/div/div/div[4]/div/div[1]/div[4]/div/div[2]/div/div[1]/div[2]/input'
exchange_fee_input_locator = //*[@id='productMainContentContainerId']/div/div[1]/div/div/div[4]/div/div[1]/div[5]/div/div/div/div[1]/div[2]/input exchange_fee_input_locator = '//*[@id='productMainContentContainerId']/div/div[1]/div/div/div[4]/div/div[1]/div[5]/div/div/div/div[1]/div[2]/input'
plus_margin_locator = //*[@id='productMainContentContainerId']/div/div[1]/div/div/div[2]/div/div[1]/div[8]/div/div/div[3]/div/div/div/div[1]/div[2]/input 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[{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 standard_selling_price_locator = '//*[@id='productMainContentContainerId']/div/div[2]/div/div/div[5]/div[1]/div/div/div/div/div[2]/table/tbody/tr[{i}]/td[4]/div/div/div[1]/div/div[2]/input'
[OptionLocators] [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[{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_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'
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'
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({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' 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'
@ -23,95 +23,112 @@ 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' 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'
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({i}) > 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"]'
[DetailLocators] [DetailLocators]
product_detail_input_locator = //*[@id='detailMainContainerId']/div/div/div[{i}]/textarea product_detail_input_locator = '//*[@id='detailMainContainerId']/div/div/div[{i}]/textarea'
product_image_locator = //*[@id='detailMainContainerId']/div/div/div[{i}]/img product_image_locator = '//*[@id='detailMainContainerId']/div/div/div[{i}]/img'
[DetailPageTextTemplates] [DetailPageTextTemplates]
leading_text_1 = "---" leading_text_1 = '---'
leading_text_2 = "# > 안녕하세요 혜리수샵입니다." leading_text_2 = '# > 안녕하세요 혜리수샵입니다.'
leading_text_3 = " " leading_text_3 = ' '
leading_text_4 = " " leading_text_4 = ' '
leading_text_5 = "### 마켓정책으로 인해 모든 옵션이 노출되지 않을수도 있습니다." leading_text_5 = '### 마켓정책으로 인해 모든 옵션이 노출되지 않을수도 있습니다.'
leading_text_6 = "**반드시 옵션사진과 옵션이름을 확인하시고 구매하시기 바랍니다.**" leading_text_6 = '**반드시 옵션사진과 옵션이름을 확인하시고 구매하시기 바랍니다.**'
leading_text_7 = "---" leading_text_7 = '---'
# 필요한 만큼 추가 가능 # 필요한 만큼 추가 가능
[TitleLocators] [TitleLocators]
# 상품명 관련 선택자 # 상품명 관련 선택자
product_name_input_locator = //*[@id='productMainContentContainerId']/div/div[1]/div[5]/div[1]/span/input product_name_input_locator = '//*[@id='productMainContentContainerId']/div/div[1]/div[5]/div[1]/span/input'
product_name_input_css_path = 'div#productMainContentContainerId div:nth-child(5) > div:nth-child(1) > span > input' product_name_input_css_path = 'div#productMainContentContainerId div:nth-child(5) > div:nth-child(1) > span > input'
# 상품명 추천단어 입력칸 선택자 # 상품명 추천단어 입력칸 선택자
product_name_suggestion_input_locator = //*[@id="productMainContentContainerId"]/div/div[1]/div[2]/div[2]/div/span/span/span[1]/input product_name_suggestion_input_locator = '//*[@id="productMainContentContainerId"]/div/div[1]/div[2]/div[2]/div/span/span/span[1]/input'
product_name_suggestion_input_css_path = 'div#productMainContentContainerId div:nth-child(2) > div:nth-child(2) > div > span > span > span.ant-input-affix-wrapper.css-1li46mu.ant-input-outlined > input' product_name_suggestion_input_css_path = 'div#productMainContentContainerId div:nth-child(2) > div:nth-child(2) > div > span > span > span.ant-input-affix-wrapper.css-1li46mu.ant-input-outlined > input'
# 상품명 추천단어 입력 검색 버튼 선택자 # 상품명 추천단어 입력 검색 버튼 선택자
product_name_search_button_locator = //*[@id="productMainContentContainerId"]/div/div[1]/div[2]/div[2]/div/span/span/span[2]/button product_name_search_button_locator = '//*[@id="productMainContentContainerId"]/div/div[1]/div[2]/div[2]/div/span/span/span[2]/button'
product_name_search_button_css_path = 'div#productMainContentContainerId div:nth-child(2) > div:nth-child(2) > div > span > span > span.ant-input-group-addon > button[type="button"]' product_name_search_button_css_path = 'div#productMainContentContainerId div:nth-child(2) > div:nth-child(2) > div > span > span > span.ant-input-group-addon > button[type="button"]'
# 원본 상품명 선택자 # 원본 상품명 선택자
original_product_name_locator = //*[@id="productMainContentContainerId"]/div/div[1]/div[6]/div[1]/div/span original_product_name_locator = '//*[@id="productMainContentContainerId"]/div/div[1]/div[6]/div[1]/div/span'
original_product_name_css_path = 'div#productMainContentContainerId div.sc-aNeao.tNLFa > div.ant-flex.css-1li46mu.ant-flex-align-stretch.ant-flex-vertical > div:nth-child(1) > div > span' original_product_name_css_path = 'div#productMainContentContainerId div.sc-aNeao.tNLFa > div.ant-flex.css-1li46mu.ant-flex-align-stretch.ant-flex-vertical > div:nth-child(1) > div > span'
# 상품명의 경고단어 삭제 버튼 선택자 # 상품명의 경고단어 삭제 버튼 선택자
product_name_warning_delete_button_locator = //*[@id="productMainContentContainerId"]/div/div[1]/div[6]/div[3]/div[2]/div/button product_name_warning_delete_button_locator = '//*[@id="productMainContentContainerId"]/div/div[1]/div[6]/div[3]/div[2]/div/button'
product_name_warning_delete_button_css_path = 'div#productMainContentContainerId div:nth-child(2) > div > button[type="button"]' product_name_warning_delete_button_css_path = 'div#productMainContentContainerId div:nth-child(2) > div > button[type="button"]'
# 카테고리 관련 선택자 # 카테고리 관련 선택자
category_suggestion_button_locator = //*[@id='productMainContentContainerId']/div/div[1]/div[5]/div[2]/button category_suggestion_button_locator = '//*[@id='productMainContentContainerId']/div/div[1]/div[5]/div[2]/button'
category_suggestion_button_css_path = 'div#productMainContentContainerId div:nth-child(2) > button[type="button"]' category_suggestion_button_css_path = 'div#productMainContentContainerId div:nth-child(2) > button[type="button"]'
# 카테고리 선택자 - 인증 여부에 따른 분기 # 카테고리 선택자 - 인증 여부에 따른 분기
category_main_selector_with_cp = '#productMainContentContainerId .ant-select.ant-select-outlined.css-1li46mu.ant-select-single.ant-select-show-arrow:nth-of-type(1)' category_main_selector_with_cp = 'div#productMainContentContainerId div.ant-select.ant-select-outlined.css-1li46mu.ant-select-single.ant-select-show-arrow >> nth=1'
category_main_selector_with_ss = '#productMainContentContainerId .ant-select.ant-select-outlined.css-1li46mu.ant-select-single.ant-select-show-arrow:nth-of-type(2)' category_main_selector_with_ss = 'div#productMainContentContainerId div.ant-select.ant-select-outlined.css-1li46mu.ant-select-single.ant-select-show-arrow >> nth=2'
category_main_selector_with_esm = '#productMainContentContainerId .ant-select.ant-select-outlined.css-1li46mu.ant-select-single.ant-select-show-arrow:nth-of-type(3)' category_main_selector_with_esm = 'div#productMainContentContainerId div.ant-select.ant-select-outlined.css-1li46mu.ant-select-single.ant-select-show-arrow >> nth=3'
category_certified_text_locator = div.ant-col.css-1li46mu:nth-child(1) ; category_main_selector_with_cp = '#productMainContentContainerId .ant-select.ant-select-outlined.css-1li46mu.ant-select-single.ant-select-show-arrow:nth-of-type(1)'
category_text_with_certification_locator = div.ant-col.css-1li46mu:nth-child(2) ; category_main_selector_with_ss = '#productMainContentContainerId .ant-select.ant-select-outlined.css-1li46mu.ant-select-single.ant-select-show-arrow:nth-of-type(2)'
category_text_without_certification_locator = div.ant-col.css-1li46mu:nth-child(1) ; category_main_selector_with_esm = '#productMainContentContainerId .ant-select.ant-select-outlined.css-1li46mu.ant-select-single.ant-select-show-arrow:nth-of-type(3)'
category_text_locator = 'div.ant-col.css-1li46mu:nth-child(1)'
category_text_locator_certified = 'div.ant-col.css-1li46mu:nth-child(2)'
; category_text_without_certification_locator = 'div.ant-col.css-1li46mu:nth-child(1)'
[BrowserControl] [BrowserControl]
# 크롬 창 이름 # 크롬 창 이름
chrome_window_name = 퍼센티 - 셀러들을 위한 AI 구매대행 솔루션 - Chrome chrome_window_name = '퍼센티 - 셀러들을 위한 AI 구매대행 솔루션 - Chrome'
# 관리자 로그인 관련 선택자 # 관리자 로그인 관련 선택자
login_email_locator = input[placeholder="이메일 주소 입력"] login_email_locator = 'input[placeholder="이메일 주소 입력"]'
login_password_locator = input[placeholder="영문/숫자/특수문자의 조합 (6~15자리)"] login_password_locator = 'input[placeholder="영문/숫자/특수문자의 조합 (6~15자리)"]'
login_button_locator = button:has-text("로그인 하기") login_button_locator = 'button:has-text("로그인 하기")'
# 직원 로그인 관련 선택자 # 직원 로그인 관련 선택자
staff_id_locator = input[placeholder="직원 아이디 입력"] staff_id_locator = 'input[placeholder="직원 아이디 입력"]'
staff_login_button_locator = button:has-text("직원 로그인 하기") staff_login_button_locator = 'button:has-text("직원 로그인 하기")'
admin_toggle_locator = button[role="switch"] admin_toggle_locator = 'button[role="switch"]'
# 광고 다이얼로그 관련 선택자 # 광고 다이얼로그 관련 선택자
close_ad_dialog_locator = div.ant-modal-wrap.ant-modal-centered close_ad_dialog_locator = 'div.ant-modal-wrap.ant-modal-centered'
close_ad_button_locator = div.ant-modal-footer > div > div > button[type='button'].ant-btn.css-1li46mu.ant-btn-default close_ad_button_locator = 'div.ant-modal-footer > div > div > button[type='button'].ant-btn.css-1li46mu.ant-btn-default'
# 상품 관련 선택자 # 상품 관련 선택자
product_name_template_xpath = /html/body/div[1]/div/div/div/div/main/div/div[2]/div[2]/div[3]/div/div/ul/div[{index}]/div/li/div/div/div[2]/div/div/div[1]/div[1]/span[2] product_name_template_xpath = '/html/body/div[1]/div/div/div/div/main/div/div[2]/div[2]/div[3]/div/div/ul/div[{index}]/div/li/div/div/div[2]/div/div/div[1]/div[1]/span[2]'
product_name_template = 'div#root div:nth-child({index}) > div > li > div > div > div:nth-child(2) > div > div > div.ant-col.css-1li46mu > div.sc-ktPPKK.ezbvYT > span.sc-ecPEgm.gmiQgL.Body3Regular14.CharacterPrimary85' product_name_template = 'div#root div:nth-child({index}) > div > li > div > div > div:nth-child(2) > div > div > div.ant-col.css-1li46mu > div.sc-ktPPKK.ezbvYT > span.sc-ecPEgm.gmiQgL.Body3Regular14.CharacterPrimary85'
product_price_template = 'div#root div:nth-child({index}) > div > li > div > div > div:nth-child(2) > div > div > div.ant-col.css-1li46mu > span.price' product_price_template = 'div#root div:nth-child({index}) > div > li > div > div > div:nth-child(2) > div > div > div.ant-col.css-1li46mu > span.price'
product_image_template = 'div#root div:nth-child({index}) > div > li > div > div > div:nth-child(2) > div > img' product_image_template = 'div#root div:nth-child({index}) > div > li > div > div > div:nth-child(2) > div > img'
# 상품 편집 및 페이지 이동 관련 선택자 # 상품 편집 및 페이지 이동 관련 선택자
product_edit_button = button:has-text("세부사항 수정 및 업로드") product_edit_button = 'button:has-text("세부사항 수정 및 업로드")'
product_edit_button_template = //button[span[text()="세부사항 수정 및 업로드"]] product_edit_button_template = '//button[span[text()="세부사항 수정 및 업로드"]]'
next_page_button_template = li.ant-pagination-item[title="{page_number}"] next_page_button_template = 'li.ant-pagination-item[title="{page_number}"]'
new_product_page_locator = span.ant-menu-title-content:has-text("신규 상품 등록") new_product_page_locator = 'span.ant-menu-title-content:has-text("신규 상품 등록")'
current_page_locator = li.ant-pagination-item.ant-pagination-item-active current_page_locator = 'li.ant-pagination-item.ant-pagination-item-active'
total_product_count_locator = '#root > div > div > div > div > main > div > div.sc-ezreuY.kYrYVh > div.sc-dChVcU.cRrUlt > div.sc-izQBue.dxiUJm > div > div:nth-child(1) > label > span:nth-child(2)'
# 편집페이지 관련 선택자
title_tab_locator = 'div.ant-tabs-tab:has-text("상품명 / 카테고리")'
option_tab_locator = 'div.ant-tabs-tab:has-text("옵션")'
price_tab_locator = 'div.ant-tabs-tab:has-text("가격")'
tag_tab_locator = 'div.ant-tabs-tab:has-text("키워드")'
thumb_tab_locator = 'div.ant-tabs-tab:has-text("썸네일")'
detail_tab_locator = 'div.ant-tabs-tab:has-text("상세페이지")'
upload_tab_locator = 'div.ant-tabs-tab:has-text("업로드")'
# 상세페이지 소스 관련 선택자 # 상세페이지 소스 관련 선택자
source_button_locator = button[data-cke-tooltip-text="소스"] source_button_locator = 'button[data-cke-tooltip-text="소스"]'
ck_source_editing_area_locator = div.ck-source-editing-area ck_source_editing_area_locator = 'div.ck-source-editing-area'
# 옵션 입력 필드 # 옵션 입력 필드
option_input_field_locator = 'div#productMainContentContainerId > div > div > div:nth-child(2) > div:nth-child(2) > div:nth-child(2) > div' option_input_field_locator = 'div#productMainContentContainerId > div > div > div:nth-child(2) > div:nth-child(2) > div:nth-child(2) > div'
# Save
save_button_locator = 'button:has-text("저장하기")'
[CategoryMargins] [CategoryMargins]
categories = 가구, 농기구 categories = 가구, 농기구

View File

@ -17,69 +17,76 @@ class LocatorManager:
# PriceLocators 섹션 # PriceLocators 섹션
self.selectors['PriceLocators'] = { self.selectors['PriceLocators'] = {
'return_fee_input_locator': self.config.get('PriceLocators', 'return_fee_input_locator'), 'return_fee_input_locator': self.config.get('PriceLocators', 'return_fee_input_locator').strip("'"),
'first_delv_fee_input_locator': self.config.get('PriceLocators', 'first_delv_fee_input_locator'), 'first_delv_fee_input_locator': self.config.get('PriceLocators', 'first_delv_fee_input_locator').strip("'"),
'exchange_fee_input_locator': self.config.get('PriceLocators', 'exchange_fee_input_locator'), 'exchange_fee_input_locator': self.config.get('PriceLocators', 'exchange_fee_input_locator').strip("'"),
'plus_margin_locator': self.config.get('PriceLocators', 'plus_margin_locator'), 'plus_margin_locator': self.config.get('PriceLocators', 'plus_margin_locator').strip("'"),
'oversea_shipping_locator': self.config.get('PriceLocators', 'oversea_shipping_locator'), 'oversea_shipping_locator': self.config.get('PriceLocators', 'oversea_shipping_locator').strip("'"),
'option_count_text_locator': self.config.get('PriceLocators', 'option_count_text_locator'), 'option_count_text_locator': self.config.get('PriceLocators', 'option_count_text_locator').strip("'"),
'product_cost_locator': self.config.get('PriceLocators', 'product_cost_locator'), 'product_cost_locator': self.config.get('PriceLocators', 'product_cost_locator').strip("'"),
'standard_selling_price_locator': self.config.get('PriceLocators', 'standard_selling_price_locator') 'standard_selling_price_locator': self.config.get('PriceLocators', 'standard_selling_price_locator').strip("'"),
} }
# BrowserControl 섹션 # BrowserControl 섹션
self.selectors['BrowserControl'] = { self.selectors['BrowserControl'] = {
'login_email_locator': self.config.get('BrowserControl', 'login_email_locator'), 'login_email_locator': self.config.get('BrowserControl', 'login_email_locator').strip("'"),
'login_password_locator': self.config.get('BrowserControl', 'login_password_locator'), 'login_password_locator': self.config.get('BrowserControl', 'login_password_locator').strip("'"),
'login_button_locator': self.config.get('BrowserControl', 'login_button_locator'), 'login_button_locator': self.config.get('BrowserControl', 'login_button_locator').strip("'"),
'admin_toggle_locator': self.config.get('BrowserControl', 'admin_toggle_locator'), 'admin_toggle_locator': self.config.get('BrowserControl', 'admin_toggle_locator').strip("'"),
'staff_id_locator': self.config.get('BrowserControl', 'staff_id_locator'), 'staff_id_locator': self.config.get('BrowserControl', 'staff_id_locator').strip("'"),
'staff_login_button_locator': self.config.get('BrowserControl', 'staff_login_button_locator'), 'staff_login_button_locator': self.config.get('BrowserControl', 'staff_login_button_locator').strip("'"),
'close_ad_dialog_locator': self.config.get('BrowserControl', 'close_ad_dialog_locator'), 'close_ad_dialog_locator': self.config.get('BrowserControl', 'close_ad_dialog_locator').strip("'"),
'close_ad_button_locator': self.config.get('BrowserControl', 'close_ad_button_locator'), 'close_ad_button_locator': self.config.get('BrowserControl', 'close_ad_button_locator').strip("'"),
'product_name_template': self.config.get('BrowserControl', 'product_name_template'), 'total_product_count_locator': self.config.get('BrowserControl', 'total_product_count_locator').strip("'"),
'product_price_template': self.config.get('BrowserControl', 'product_price_template'), 'product_name_template': self.config.get('BrowserControl', 'product_name_template').strip("'"),
'product_image_template': self.config.get('BrowserControl', 'product_image_template'), 'product_price_template': self.config.get('BrowserControl', 'product_price_template').strip("'"),
'new_product_page_locator': self.config.get('BrowserControl', 'new_product_page_locator'), 'product_image_template': self.config.get('BrowserControl', 'product_image_template').strip("'"),
'current_page_locator': self.config.get('BrowserControl', 'current_page_locator'), 'new_product_page_locator': self.config.get('BrowserControl', 'new_product_page_locator').strip("'"),
'next_page_button_template': self.config.get('BrowserControl', 'next_page_button_template'), 'current_page_locator': self.config.get('BrowserControl', 'current_page_locator').strip("'"),
'source_button_locator': self.config.get('BrowserControl', 'source_button_locator'), 'next_page_button_template': self.config.get('BrowserControl', 'next_page_button_template').strip("'"),
'ck_source_editing_area_locator': self.config.get('BrowserControl', 'ck_source_editing_area_locator'), 'source_button_locator': self.config.get('BrowserControl', 'source_button_locator').strip("'"),
'option_input_field_locator': self.config.get('BrowserControl', 'option_input_field_locator') 'ck_source_editing_area_locator': self.config.get('BrowserControl', 'ck_source_editing_area_locator').strip("'"),
'title_tab_locator': self.config.get('BrowserControl', 'title_tab_locator').strip("'"),
'option_tab_locator': self.config.get('BrowserControl', 'option_tab_locator').strip("'"),
'price_tab_locator': self.config.get('BrowserControl', 'price_tab_locator').strip("'"),
'tag_tab_locator': self.config.get('BrowserControl', 'tag_tab_locator').strip("'"),
'thumb_tab_locator': self.config.get('BrowserControl', 'thumb_tab_locator').strip("'"),
'detail_tab_locator': self.config.get('BrowserControl', 'detail_tab_locator').strip("'"),
'upload_tab_locator': self.config.get('BrowserControl', 'upload_tab_locator').strip("'"),
'save_button_locator': self.config.get('BrowserControl', 'save_button_locator').strip("'"),
} }
# OptionLocators 섹션 # OptionLocators 섹션
self.selectors['OptionLocators'] = { self.selectors['OptionLocators'] = {
'option_excluded_selector_template': self.config.get('OptionLocators', 'option_excluded_selector_template'), 'option_excluded_selector_template': self.config.get('OptionLocators', 'option_excluded_selector_template').strip("'"),
'option_input_selector_template': self.config.get('OptionLocators', 'option_input_selector_template'), 'option_input_selector_template': self.config.get('OptionLocators', 'option_input_selector_template').strip("'"),
'single_option_locator': self.config.get('OptionLocators', 'single_option_locator'), 'single_option_locator': self.config.get('OptionLocators', 'single_option_locator').strip("'"),
'option_product_locator': self.config.get('OptionLocators', 'option_product_locator'), 'option_product_locator': self.config.get('OptionLocators', 'option_product_locator').strip("'"),
'total_options_selector': self.config.get('OptionLocators', 'total_options_selector'), 'total_options_selector': self.config.get('OptionLocators', 'total_options_selector').strip("'"),
'original_name_selector_template': self.config.get('OptionLocators', 'original_name_selector_template'), 'original_name_selector_template': self.config.get('OptionLocators', 'original_name_selector_template').strip("'"),
'edit_field_selector_template': self.config.get('OptionLocators', 'edit_field_selector_template'), 'edit_field_selector_template': self.config.get('OptionLocators', 'edit_field_selector_template').strip("'"),
'checkbox_selector_template': self.config.get('OptionLocators', 'checkbox_selector_template'), 'checkbox_selector_template': self.config.get('OptionLocators', 'checkbox_selector_template').strip("'"),
'image_selector_template': self.config.get('OptionLocators', 'image_selector_template'), 'image_selector_template': self.config.get('OptionLocators', 'image_selector_template').strip("'"),
'price_selector_template': self.config.get('OptionLocators', 'price_selector_template'), 'price_selector_template': self.config.get('OptionLocators', 'price_selector_template').strip("'"),
'delete_button_selector_template': self.config.get('OptionLocators', 'delete_button_selector_template'), 'delete_button_selector_template': self.config.get('OptionLocators', 'delete_button_selector_template').strip("'"),
'confirm_delete_button_locator': self.config.get('OptionLocators', 'confirm_delete_button_locator'), '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'), 'add_button_selector_template': self.config.get('OptionLocators', 'add_button_selector_template').strip("'"),
'file_input_locator': self.config.get('OptionLocators', 'file_input_locator') 'file_input_locator': self.config.get('OptionLocators', 'file_input_locator').strip("'"),
} }
# TitleLocators 섹션 # TitleLocators 섹션
self.selectors['TitleLocators'] = { self.selectors['TitleLocators'] = {
'product_name_input_locator': self.config.get('TitleLocators', 'product_name_input_locator'), 'product_name_input_locator': self.config.get('TitleLocators', 'product_name_input_locator').strip("'"),
'product_name_suggestion_input_locator': self.config.get('TitleLocators', 'product_name_suggestion_input_locator'), 'product_name_suggestion_input_locator': self.config.get('TitleLocators', 'product_name_suggestion_input_locator').strip("'"),
'product_name_search_button_locator': self.config.get('TitleLocators', 'product_name_search_button_locator'), 'product_name_search_button_locator': self.config.get('TitleLocators', 'product_name_search_button_locator').strip("'"),
'original_product_name_locator': self.config.get('TitleLocators', 'original_product_name_locator'), 'original_product_name_locator': self.config.get('TitleLocators', 'original_product_name_locator').strip("'"),
'product_name_warning_delete_button_locator': self.config.get('TitleLocators', 'product_name_warning_delete_button_locator'), 'product_name_warning_delete_button_locator': self.config.get('TitleLocators', 'product_name_warning_delete_button_locator').strip("'"),
'category_suggestion_button_locator': self.config.get('TitleLocators', 'category_suggestion_button_locator'), 'category_suggestion_button_locator': self.config.get('TitleLocators', 'category_suggestion_button_locator').strip("'"),
'category_main_selector_with_cp': self.config.get('TitleLocators', 'category_main_selector_with_cp'), 'category_main_selector_with_cp': self.config.get('TitleLocators', 'category_main_selector_with_cp').strip("'"),
'category_main_selector_with_ss': self.config.get('TitleLocators', 'category_main_selector_with_ss'), 'category_main_selector_with_ss': self.config.get('TitleLocators', 'category_main_selector_with_ss').strip("'"),
'category_main_selector_with_esm': self.config.get('TitleLocators', 'category_main_selector_with_esm'), 'category_main_selector_with_esm': self.config.get('TitleLocators', 'category_main_selector_with_esm').strip("'"),
'category_certified_text_locator': self.config.get('TitleLocators', 'category_certified_text_locator'), 'category_text_locator': self.config.get('TitleLocators', 'category_text_locator').strip("'"),
'category_text_with_certification_locator': self.config.get('TitleLocators', 'category_text_with_certification_locator'), 'category_text_locator_certified': self.config.get('TitleLocators', 'category_text_locator_certified').strip("'"),
'category_text_without_certification_locator': self.config.get('TitleLocators', 'category_text_without_certification_locator'),
} }

View File

@ -1,7 +1,7 @@
from PySide6.QtWidgets import (QDialog, QFileDialog, QVBoxLayout, QHBoxLayout, QGridLayout, QTreeWidget, QTreeWidgetItem, QLabel, QLineEdit, QPushButton, from PySide6.QtWidgets import (QDialog, QFileDialog, QVBoxLayout, QHBoxLayout, QGridLayout, QTreeWidget, QTreeWidgetItem, QLabel, QLineEdit, QPushButton,
QSpinBox, QGroupBox, QFileDialog, QMessageBox, QCheckBox) QSpinBox, QGroupBox, QFileDialog, QMessageBox, QComboBox)
from PySide6.QtCore import Qt from PySide6.QtCore import Qt
from PySide6.QtGui import QFont from PySide6.QtGui import QFont, QColor
import sqlite3 import sqlite3
import shutil import shutil
import os, re import os, re
@ -32,12 +32,12 @@ class CMBSettingsDialog(QDialog):
left_layout = QVBoxLayout() left_layout = QVBoxLayout()
mid_layout = QVBoxLayout() mid_layout = QVBoxLayout()
right_layout = QVBoxLayout() self.right_layout = QVBoxLayout()
# 카테고리 검색 및 단계 설정 버튼 # 카테고리 검색 및 단계 설정 버튼
search_layout = QHBoxLayout() search_layout = QGridLayout()
search_layout.addWidget(QLabel("카테고리 검색:")) search_layout.addWidget(QLabel("카테고리 검색:"),0,0,1,1)
self.search_input = QLineEdit() self.search_input = QLineEdit()
self.search_input.returnPressed.connect(self.search_category) self.search_input.returnPressed.connect(self.search_category)
self.search_btn = QPushButton("검색") self.search_btn = QPushButton("검색")
@ -46,23 +46,69 @@ class CMBSettingsDialog(QDialog):
self.search_btn.setDefault(False) self.search_btn.setDefault(False)
self.search_btn.clicked.connect(self.search_category) self.search_btn.clicked.connect(self.search_category)
search_layout.addWidget(self.search_input) search_layout.addWidget(self.search_input,0,1,1,3)
search_layout.addWidget(self.search_btn) search_layout.addWidget(self.search_btn,0,2,1,1)
self.cmb_view_btn = QPushButton("모두 보기") self.cmb_view_btn = QPushButton("모두 보기")
self.cmb_view_btn.clicked.connect(self.toggle_cmb_view) self.cmb_view_btn.clicked.connect(self.toggle_cmb_view)
search_layout.addWidget(self.cmb_view_btn) search_layout.addWidget(self.cmb_view_btn,0,3,1,1)
# 1레벨, 2레벨, 3레벨 콤보박스와 라벨 설정
self.level1_combo = QComboBox()
self.level2_combo = QComboBox()
self.level3_combo = QComboBox()
self.reset_combo_btn = QPushButton("콤보리셋")
# 기본적으로 "모두 보기" 옵션 추가
self.level1_combo.addItem("모두 보기")
self.level2_combo.addItem("모두 보기")
self.level3_combo.addItem("모두 보기")
# 레벨 필터링 라벨 추가
# search_layout.addWidget(QLabel("1레벨:"))
search_layout.addWidget(self.level1_combo,1,1,1,2)
# search_layout.addWidget(QLabel("2레벨:"))
search_layout.addWidget(self.level2_combo,1,2,1,2)
# search_layout.addWidget(QLabel("3레벨:"))
search_layout.addWidget(self.level3_combo,1,3,1,2)
search_layout.addWidget(self.level3_combo,1,3,1,2)
search_layout.addWidget(self.reset_combo_btn,1,4,1,1)
# 콤보박스의 신호 연결
self.level1_combo.currentTextChanged.connect(self.update_level2_combo)
self.level1_combo.currentTextChanged.connect(self.filter_category_tree)
self.level2_combo.currentTextChanged.connect(self.update_level3_combo)
self.level2_combo.currentTextChanged.connect(self.filter_category_tree)
self.level3_combo.currentTextChanged.connect(self.filter_category_tree)
# 콤보박스 리셋
self.reset_combo_btn.clicked.connect(self.reset_comboboxes)
# # 카테고리 레벨별 필터링을 위한 콤보박스
# filter_label = QLabel("카테고리 필터:")
# filter_combo = QComboBox()
# filter_combo.addItem("모두 보기")
# filter_combo.addItem("Level 1")
# filter_combo.addItem("Level 2")
# filter_combo.addItem("Level 3")
# filter_combo.addItem("Level 4")
# filter_combo.currentIndexChanged.connect(self.filter_categories)
# search_layout.addWidget(filter_label,1,0,1,1)
# search_layout.addWidget(filter_combo)
left_layout.addLayout(search_layout, 1) left_layout.addLayout(search_layout, 1)
# 카테고리 목록 테이블 # 카테고리 목록 테이블
self.category_tree = QTreeWidget() self.category_tree = QTreeWidget()
self.category_tree.setHeaderLabels(["Level1", "Level2", "Level3", "Level4", "CMB 단계"]) self.category_tree.setHeaderLabels(["ID", "Level1", "Level2", "Level3", "Level4", "CMB 단계"])
self.category_tree.setColumnCount(5) self.category_tree.setColumnCount(6)
self.category_tree.setRootIsDecorated(False) self.category_tree.setRootIsDecorated(False)
self.category_tree.setAlternatingRowColors(True) self.category_tree.setAlternatingRowColors(True)
self.category_tree.setSortingEnabled(True) # 정렬 기능 활성화 self.category_tree.setSortingEnabled(True) # 정렬 기능 활성화
# 정렬 순서 추적 # 정렬 순서 추적
self.sort_order = Qt.AscendingOrder self.sort_order = Qt.AscendingOrder
# 헤더 클릭 시그널 연결 # 헤더 클릭 시그널 연결
self.category_tree.header().sectionClicked.connect(self.sort_by_column) self.category_tree.header().sectionClicked.connect(self.sort_by_column)
@ -83,12 +129,21 @@ class CMBSettingsDialog(QDialog):
apply_1_btn = QPushButton("1단계 적용") apply_1_btn = QPushButton("1단계 적용")
apply_2_btn = QPushButton("2단계 적용") apply_2_btn = QPushButton("2단계 적용")
apply_3_btn = QPushButton("3단계 적용") apply_3_btn = QPushButton("3단계 적용")
remove_cmb_stage_button = QPushButton("선택 CMB 해제")
self.select_toggle_button = QPushButton("전체 선택")
apply_1_btn.clicked.connect(lambda: self.apply_crmobi_stage(1)) apply_1_btn.clicked.connect(lambda: self.apply_crmobi_stage(1))
apply_2_btn.clicked.connect(lambda: self.apply_crmobi_stage(2)) apply_2_btn.clicked.connect(lambda: self.apply_crmobi_stage(2))
apply_3_btn.clicked.connect(lambda: self.apply_crmobi_stage(3)) apply_3_btn.clicked.connect(lambda: self.apply_crmobi_stage(3))
remove_cmb_stage_button.clicked.connect(self.remove_cmb_stage)
self.select_toggle_button.clicked.connect(self.toggle_select_all_filtered_items)
mid_layout.addWidget(apply_1_btn) mid_layout.addWidget(apply_1_btn)
mid_layout.addWidget(apply_2_btn) mid_layout.addWidget(apply_2_btn)
mid_layout.addWidget(apply_3_btn) mid_layout.addWidget(apply_3_btn)
mid_layout.addWidget(remove_cmb_stage_button)
mid_layout.addWidget(self.select_toggle_button)
# 닫기 버튼 # 닫기 버튼
close_btn = QPushButton("닫기") close_btn = QPushButton("닫기")
@ -105,8 +160,14 @@ class CMBSettingsDialog(QDialog):
# CMB 단계 설정 그룹 추가 # CMB 단계 설정 그룹 추가
self.cmb_settings_group = QGroupBox("CMB 단계 설정") self.cmb_settings_group = QGroupBox("CMB 단계 설정")
cmb_settings_layout = QVBoxLayout() cmb_settings_layout = QVBoxLayout()
# CMB 단계를 설정 후 저장하는 버튼
save_cmb_stage_button = QPushButton("CMB 단계 저장")
save_cmb_stage_button.clicked.connect(self.save_cmb_stage_to_db)
cmb_settings_layout.addWidget(save_cmb_stage_button)
# 각 단계별 설정 # 각 단계별 설정
self.stage_widgets = [] self.stage_widgets = []
for i in range(1, 4): for i in range(1, 4):
@ -154,12 +215,12 @@ class CMBSettingsDialog(QDialog):
cost_spin.setSingleStep(1000) cost_spin.setSingleStep(1000)
# QGroupBox 레이아웃 구성 # QGroupBox 레이아웃 구성
stage_group_box_layout.addWidget(min_amount_spin,0,0) stage_group_box_layout.addWidget(min_amount_spin,0,0,1,2)
stage_group_box_layout.addWidget(min_amount_label,0,1) stage_group_box_layout.addWidget(min_amount_label,1,1)
stage_group_box_layout.addWidget(unit_amount_spin,1,0) stage_group_box_layout.addWidget(unit_amount_spin,2,0,1,2)
stage_group_box_layout.addWidget(unit_amount_label,1,1) stage_group_box_layout.addWidget(unit_amount_label,3,1)
stage_group_box_layout.addWidget(cost_spin,2,0) stage_group_box_layout.addWidget(cost_spin,4,0,1,2)
stage_group_box_layout.addWidget(cost_label,2,1) stage_group_box_layout.addWidget(cost_label,5,1)
stage_group_box.setLayout(stage_group_box_layout) stage_group_box.setLayout(stage_group_box_layout)
# 전체 레이아웃에 QGroupBox 추가 # 전체 레이아웃에 QGroupBox 추가
@ -169,13 +230,144 @@ class CMBSettingsDialog(QDialog):
self.stage_widgets.append((min_amount_spin, unit_amount_spin, cost_spin)) self.stage_widgets.append((min_amount_spin, unit_amount_spin, cost_spin))
self.cmb_settings_group.setLayout(cmb_settings_layout) self.cmb_settings_group.setLayout(cmb_settings_layout)
right_layout.addWidget(self.cmb_settings_group) self.right_layout.addWidget(self.cmb_settings_group)
main_layout.addLayout(right_layout,2) main_layout.addLayout(self.right_layout,3)
self.setLayout(main_layout) self.setLayout(main_layout)
# DB 읽어와서 테이블에 표시 # DB 읽어와서 테이블에 표시
self.load_db_to_table() self.load_db_to_table()
self.set_column_widths()
self.update_cmb_settings_from_db()
self.load_level1_categories()
def load_level1_categories(self):
"""1레벨 카테고리를 DB에서 로드하여 콤보박스에 추가"""
query = "SELECT DISTINCT category1 FROM categories WHERE category1 IS NOT NULL"
self.cursor.execute(query)
rows = self.cursor.fetchall()
for row in rows:
self.level1_combo.addItem(row[0])
def update_level2_combo(self):
"""1레벨 선택 시, 2레벨 콤보박스를 업데이트"""
selected_level1 = self.level1_combo.currentText()
self.level2_combo.clear()
self.level2_combo.addItem("모두 보기")
if selected_level1 == "모두 보기":
return
query = "SELECT DISTINCT category2 FROM categories WHERE category1 = ? AND category2 IS NOT NULL"
self.cursor.execute(query, (selected_level1,))
rows = self.cursor.fetchall()
for row in rows:
self.level2_combo.addItem(row[0])
def update_level3_combo(self):
"""2레벨 선택 시, 3레벨 콤보박스를 업데이트"""
selected_level1 = self.level1_combo.currentText()
selected_level2 = self.level2_combo.currentText()
self.level3_combo.clear()
self.level3_combo.addItem("모두 보기")
if selected_level2 == "모두 보기":
return
query = "SELECT DISTINCT category3 FROM categories WHERE category1 = ? AND category2 = ? AND category3 IS NOT NULL"
self.cursor.execute(query, (selected_level1, selected_level2))
rows = self.cursor.fetchall()
for row in rows:
self.level3_combo.addItem(row[0])
def reset_comboboxes(self):
"""1레벨, 2레벨, 3레벨 콤보박스를 초기화하고 QTreeWidget을 전체 항목으로 필터링합니다."""
# 각 레벨 콤보박스 초기화
self.level1_combo.setCurrentIndex(0)
self.level2_combo.clear()
self.level3_combo.clear()
# QTreeWidget 필터링 초기화 (모든 항목 표시)
self.load_db_to_table() # 이 메서드는 전체 데이터를 다시 로드하는 메서드입니다.
def filter_category_tree(self):
"""선택된 1레벨, 2레벨, 3레벨 카테고리를 기준으로 트리뷰를 필터링"""
selected_level1 = self.level1_combo.currentText()
selected_level2 = self.level2_combo.currentText()
selected_level3 = self.level3_combo.currentText()
# 기본 쿼리와 조건을 설정
query = '''SELECT id, category1, category2, category3, category4, crmobi_stage FROM categories WHERE 1=1'''
args = []
# 선택된 값에 따라 필터 추가
if selected_level1 != "모두 보기":
query += " AND category1 = ?"
args.append(selected_level1)
if selected_level2 != "모두 보기":
query += " AND category2 = ?"
args.append(selected_level2)
if selected_level3 != "모두 보기":
query += " AND category3 = ?"
args.append(selected_level3)
self.cursor.execute(query, args)
rows = self.cursor.fetchall()
# 트리뷰에 표시
self.category_tree.clear()
for row_data in rows:
row_id, category1, category2, category3, category4, crmobi_stage = row_data
formatted_id = str(row_id).zfill(4)
top_item = QTreeWidgetItem([
formatted_id,
category1 or "", category2 or "",
category3 or "", category4 or "",
f"{crmobi_stage}" if crmobi_stage > 0 else "미적용"
])
top_item.setCheckState(0, Qt.Unchecked)
# 단계별 배경색 설정 QColor(R,G,B,A)
if crmobi_stage == 1:
color = QColor(152, 251, 152, 204) # 옐로우그린
elif crmobi_stage == 2:
color = QColor(135, 206, 235, 204) # 스카이블루
elif crmobi_stage == 3:
color = QColor(255, 192, 203, 204) # 라이트핑크
else:
color = None
# 행 전체에 배경색 적용
if color:
for col in range(6): # 열의 개수에 따라 반복
top_item.setBackground(col, color)
self.category_tree.addTopLevelItem(top_item)
def update_cmb_settings_from_db(self):
"""DB의 crmobi_stages 테이블에서 값을 읽어와 각 단계별 설정 위젯에 반영합니다."""
try:
# CrMoBi 단계 설정을 crmobi_stages 테이블에서 가져옴
self.cursor.execute("SELECT stage, threshold, increment_unit, extra_cost FROM crmobi_stages")
stages = self.cursor.fetchall()
# 각 단계별 설정 값을 위젯에 적용
for stage in stages:
stage_index = stage[0] - 1 # 단계가 1부터 시작하므로 인덱스를 맞추기 위해 -1
min_amount, unit_amount, extra_cost = stage[1], stage[2], stage[3]
# 위젯 리스트에서 해당 단계를 찾아서 설정
self.stage_widgets[stage_index][0].setValue(min_amount)
self.stage_widgets[stage_index][1].setValue(unit_amount)
self.stage_widgets[stage_index][2].setValue(extra_cost)
self.logger.debug("CrMoBi 단계 설정이 위젯에 반영되었습니다.")
except Exception as e:
self.logger.error(f"CrMoBi 단계 설정을 위젯에 반영하는 중 오류 발생: {e}", exc_info=True)
def create_tables(self): def create_tables(self):
"""초기 DB를 생성하고 CrMoBi 단계 테이블도 추가합니다.""" """초기 DB를 생성하고 CrMoBi 단계 테이블도 추가합니다."""
@ -191,6 +383,7 @@ class CMBSettingsDialog(QDialog):
# 테이블 생성 # 테이블 생성
cursor.execute(''' cursor.execute('''
CREATE TABLE IF NOT EXISTS categories ( CREATE TABLE IF NOT EXISTS categories (
id INTEGER PRIMARY KEY AUTOINCREMENT,
category1 TEXT, category1 TEXT,
category2 TEXT, category2 TEXT,
category3 TEXT, category3 TEXT,
@ -237,6 +430,9 @@ class CMBSettingsDialog(QDialog):
''') ''')
# 초기 데이터 설정 (원하는 경우) # 초기 데이터 설정 (원하는 경우)
cursor.execute('DELETE FROM crmobi_stages')
conn.commit()
for i in range(1, 4): for i in range(1, 4):
cursor.execute(''' cursor.execute('''
INSERT INTO crmobi_stages (stage, threshold, increment_unit, extra_cost) INSERT INTO crmobi_stages (stage, threshold, increment_unit, extra_cost)
@ -254,7 +450,7 @@ class CMBSettingsDialog(QDialog):
self.category_tree.clear() self.category_tree.clear()
# 기본 쿼리와 조건을 설정 # 기본 쿼리와 조건을 설정
query = '''SELECT category1, category2, category3, category4, crmobi_stage FROM categories WHERE 1=1''' query = '''SELECT id, category1, category2, category3, category4, crmobi_stage FROM categories WHERE 1=1'''
args = [] args = []
# 검색어가 있을 경우 WHERE 조건 추가 # 검색어가 있을 경우 WHERE 조건 추가
@ -270,64 +466,151 @@ class CMBSettingsDialog(QDialog):
self.cursor.execute(query, args) self.cursor.execute(query, args)
rows = self.cursor.fetchall() rows = self.cursor.fetchall()
for row_data in rows: # id 기준 오름차순 정렬을 위해 정렬
top_item = QTreeWidgetItem([str(data) if data else "" for data in row_data[:4]]) rows = sorted(rows, key=lambda x: int(x[0])) # x[0]는 id 열
crmobi_stage = row_data[4]
top_item.setCheckState(0, Qt.Unchecked)
top_item.setText(4, f"{crmobi_stage}" if crmobi_stage > 0 else "미적용")
# 단계별 배경색 설정 for row_data in rows:
row_id, category1, category2, category3, category4, crmobi_stage = row_data
formatted_id = str(row_id).zfill(4)
top_item = QTreeWidgetItem([formatted_id, category1 or "", category2 or "", category3 or "", category4 or "", f"{crmobi_stage}" if crmobi_stage > 0 else "미적용"])
top_item.setCheckState(0, Qt.Unchecked)
# id 값을 정수로 설정하여 정렬 시 정수 기준으로 처리되도록 함
top_item.setData(0, Qt.UserRole, int(row_id))
# 단계별 배경색 설정 QColor(R,G,B,A)
if crmobi_stage == 1: if crmobi_stage == 1:
top_item.setBackground(4, Qt.yellow) color = QColor(152, 251, 152, 204) # 옐로우그린
elif crmobi_stage == 2: elif crmobi_stage == 2:
top_item.setBackground(4, Qt.green) color = QColor(135, 206, 235, 204) # 스카이블루
elif crmobi_stage == 3: elif crmobi_stage == 3:
top_item.setBackground(4, Qt.red) color = QColor(255, 192, 203, 204) # 라이트핑크
else:
color = None
# 행 전체에 배경색 적용
if color:
for col in range(6): # 열의 개수에 따라 반복
top_item.setBackground(col, color)
self.category_tree.addTopLevelItem(top_item) self.category_tree.addTopLevelItem(top_item)
# 초기 정렬 기준을 id 열로 설정하고 오름차순 정렬
self.category_tree.setSortingEnabled(True)
self.category_tree.sortByColumn(0, Qt.AscendingOrder) # ID 열을 기준으로 오름차순 정렬
def set_column_widths(self):
"""ID 열 너비를 일정하게 설정"""
current_id_width = self.category_tree.columnWidth(0)
self.category_tree.setColumnWidth(0, int(current_id_width / 1.5)) # ID 열을 초기 설정 크기로 유지
def sort_by_column(self, index): def sort_by_column(self, index):
"""클릭된 열을 기준으로 오름차순/내림차순으로 정렬""" """클릭된 열을 기준으로 오름차순/내림차순으로 정렬"""
self.category_tree.sortByColumn(index, self.sort_order) # 정렬 역할을 UserRole로 설정하여 ID 필드가 정수로 정렬되도록 설정
self.category_tree.setSortingEnabled(False) # 정렬을 일시적으로 비활성화
self.category_tree.sortItems(index, self.sort_order)
self.category_tree.setSortingEnabled(True) # 정렬을 다시 활성화
# 정렬 순서를 토글 # 정렬 순서를 토글
self.sort_order = Qt.DescendingOrder if self.sort_order == Qt.AscendingOrder else Qt.AscendingOrder self.sort_order = Qt.DescendingOrder if self.sort_order == Qt.AscendingOrder else Qt.AscendingOrder
def apply_crmobi_stage(self, stage): def apply_crmobi_stage(self, stage):
"""선택된 카테고리에 CrMoBi 단계를 적용합니다.""" """선택된 카테고리에 CrMoBi 단계를 적용하고 DB에 저장."""
for i in range(self.category_tree.topLevelItemCount()): for i in range(self.category_tree.topLevelItemCount()):
item = self.category_tree.topLevelItem(i) item = self.category_tree.topLevelItem(i)
if item.checkState(0) == Qt.Checked: if item.checkState(0) == Qt.Checked:
category_values = [item.text(j) for j in range(4)] category_values = [item.text(j) for j in range(1, 5)] # ID 열 제외
# DB 업데이트 # DB 업데이트
self.cursor.execute('''UPDATE categories self.cursor.execute('''UPDATE categories
SET crmobi_stage = ? SET crmobi_stage = ?
WHERE category1 = ? AND category2 = ? AND category3 = ? AND category4 = ?''', WHERE category1 = ? AND category2 = ? AND category3 = ? AND category4 = ?''',
[stage] + category_values) [stage] + category_values)
# 트리뷰 업데이트
item.setText(4, str(stage))
if stage == 1:
item.setBackground(4, Qt.yellow)
elif stage == 2:
item.setBackground(4, Qt.green)
elif stage == 3:
item.setBackground(4, Qt.red)
self.conn.commit() # 변경사항 저장 self.conn.commit() # 변경사항 저장
self.load_db_to_table() # 트리 새로고침 self.load_db_to_table() # 트리 새로고침
def toggle_cmb_settings(self, checked): def remove_cmb_stage(self):
"""CMB 단계 설정 영역 표시/숨기기""" """선택된 카테고리의 CMB 단계를 해제하고 DB에 반영."""
self.cmb_settings_group.setVisible(checked) for i in range(self.category_tree.topLevelItemCount()):
item = self.category_tree.topLevelItem(i)
if item.checkState(0) == Qt.Checked:
category_id = int(item.text(0)) # ID 열에서 값 가져오기
# DB에서 CMB 단계를 해제
self.cursor.execute("UPDATE categories SET crmobi_stage = 0 WHERE id = ?", (category_id,))
self.conn.commit()
self.load_db_to_table() # 트리 새로고침
def toggle_cmb_settings(self, checked):
"""CMB 단계 설정 영역 표시/숨기기, 공간을 완전히 제거 또는 복원합니다."""
sender = self.sender() sender = self.sender()
if checked: if checked:
# 공간에 cmb_settings_group 추가
self.right_layout.addWidget(self.cmb_settings_group)
self.cmb_settings_group.show()
sender.setText("CMB 단계 설정 ▼") sender.setText("CMB 단계 설정 ▼")
else: else:
# cmb_settings_group 숨기기 및 제거
self.cmb_settings_group.hide()
self.right_layout.removeWidget(self.cmb_settings_group)
sender.setText("CMB 단계 설정 ▶") sender.setText("CMB 단계 설정 ▶")
# 레이아웃을 다시 갱신하여 공간을 완전히 반영
self.layout().update()
def toggle_select_all_filtered_items(self):
"""버튼의 텍스트에 따라 전체 선택 또는 전체 해제를 수행합니다."""
if self.select_toggle_button.text() == "전체 선택":
# 전체 체크
for i in range(self.category_tree.topLevelItemCount()):
item = self.category_tree.topLevelItem(i)
item.setCheckState(0, Qt.Checked)
# 버튼 텍스트를 "전체 해제"로 변경
self.select_toggle_button.setText("전체 해제")
else:
# 전체 체크 해제
for i in range(self.category_tree.topLevelItemCount()):
item = self.category_tree.topLevelItem(i)
item.setCheckState(0, Qt.Unchecked)
# 버튼 텍스트를 "전체 선택"으로 변경
self.select_toggle_button.setText("전체 선택")
def save_cmb_stage_to_db(self):
"""사용자가 설정한 CMB 단계를 crmobi_stage 테이블에 저장합니다."""
try:
# 각 CMB 단계의 설정을 불러옵니다
for i, (min_amount_spin, unit_amount_spin, cost_spin) in enumerate(self.stage_widgets, start=1):
stage = i
threshold = min_amount_spin.value()
increment_unit = unit_amount_spin.value()
extra_cost = cost_spin.value()
# 기존 데이터가 있는지 확인
self.cursor.execute("SELECT COUNT(1) FROM crmobi_stages WHERE stage = ?", (stage,))
exists = self.cursor.fetchone()[0]
# 데이터를 업데이트 또는 삽입
if exists:
self.cursor.execute('''
UPDATE crmobi_stages
SET threshold = ?, increment_unit = ?, extra_cost = ?
WHERE stage = ?
''', (threshold, increment_unit, extra_cost, stage))
else:
self.cursor.execute('''
INSERT INTO crmobi_stages (stage, threshold, increment_unit, extra_cost)
VALUES (?, ?, ?, ?)
''', (stage, threshold, increment_unit, extra_cost))
# 변경사항 저장
self.conn.commit()
QMessageBox.information(self, "저장 성공", "CMB 단계 설정이 성공적으로 저장되었습니다.")
except Exception as e:
QMessageBox.critical(self, "저장 오류", f"저장 중 오류가 발생했습니다: {e}")
self.logger.error(f"CMB 단계 저장 중 오류: {e}", exc_info=True)
def reset_db(self): def reset_db(self):
"""사용자 DB를 삭제하고 초기 DB를 로드합니다.""" """사용자 DB를 삭제하고 초기 DB를 로드합니다."""
if os.path.exists(self.user_db_path): if os.path.exists(self.user_db_path):
@ -386,7 +669,7 @@ class CMBSettingsDialog(QDialog):
if search_text: if search_text:
# 검색어가 있는 경우 필터링하여 로드 # 검색어가 있는 경우 필터링하여 로드
query = ''' query = '''
SELECT category1, category2, category3, category4, crmobi_stage SELECT id, category1, category2, category3, category4, crmobi_stage
FROM categories FROM categories
WHERE category1 LIKE ? OR category2 LIKE ? OR category3 LIKE ? OR category4 LIKE ? WHERE category1 LIKE ? OR category2 LIKE ? OR category3 LIKE ? OR category4 LIKE ?
''' '''
@ -395,7 +678,7 @@ class CMBSettingsDialog(QDialog):
else: else:
# 검색어가 없는 경우 전체 로드 # 검색어가 없는 경우 전체 로드
query = ''' query = '''
SELECT category1, category2, category3, category4, crmobi_stage SELECT id, category1, category2, category3, category4, crmobi_stage
FROM categories FROM categories
''' '''
self.cursor.execute(query) self.cursor.execute(query)
@ -405,18 +688,29 @@ class CMBSettingsDialog(QDialog):
rows = self.cursor.fetchall() rows = self.cursor.fetchall()
for row_data in rows: for row_data in rows:
top_item = QTreeWidgetItem([str(data) if data else "" for data in row_data[:4]]) # top_item = QTreeWidgetItem([str(data) if data else "" for data in row_data[:4]])
crmobi_stage = row_data[4] row_id, category1, category2, category3, category4, crmobi_stage = row_data
top_item.setCheckState(0, Qt.Unchecked) formatted_id = str(row_id).zfill(4)
top_item.setText(4, f"{crmobi_stage}" if crmobi_stage > 0 else "미적용") top_item = QTreeWidgetItem([formatted_id, category1 or "", category2 or "", category3 or "", category4 or "", f"{crmobi_stage}" if crmobi_stage > 0 else "미적용"])
# 단계별 배경색 설정 crmobi_stage = row_data[5]
top_item.setCheckState(0, Qt.Unchecked)
top_item.setText(5, f"{crmobi_stage}" if crmobi_stage > 0 else "미적용")
# 단계별 배경색 설정 QColor(R,G,B,A)
if crmobi_stage == 1: if crmobi_stage == 1:
top_item.setBackground(4, Qt.yellow) color = QColor(152, 251, 152, 204) # 옐로우그린
elif crmobi_stage == 2: elif crmobi_stage == 2:
top_item.setBackground(4, Qt.green) color = QColor(135, 206, 235, 204) # 스카이블루
elif crmobi_stage == 3: elif crmobi_stage == 3:
top_item.setBackground(4, Qt.red) color = QColor(255, 192, 203, 204) # 라이트핑크
else:
color = None
# 행 전체에 배경색 적용
if color:
for col in range(6): # 열의 개수에 따라 반복
top_item.setBackground(col, color)
self.category_tree.addTopLevelItem(top_item) self.category_tree.addTopLevelItem(top_item)
@ -489,7 +783,7 @@ class CustomSpinBox(QSpinBox):
# 폰트 설정 # 폰트 설정
font = QFont() font = QFont()
font.setPointSize(12) # 폰트 크기 설정 font.setPointSize(10) # 폰트 크기 설정
font.setBold(True) # 폰트 굵게 설정 font.setBold(True) # 폰트 굵게 설정
self.setFont(font) self.setFont(font)

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -11,7 +11,8 @@ class CategoryHandler:
async def handle_category_action(self): async def handle_category_action(self):
# #productMainContentContainerId 내부에서 클래스 이름 "ant-select ant-select-outlined css-1li46mu ant-select-single ant-select-show-arrow"를 포함한 요소 중 두 번째 요소 찾기 # #productMainContentContainerId 내부에서 클래스 이름 "ant-select ant-select-outlined css-1li46mu ant-select-single ant-select-show-arrow"를 포함한 요소 중 두 번째 요소 찾기
print("[DEBUG] handle_category_action: Locating category container element...") print("[DEBUG] handle_category_action: Locating category container element...")
category_locator = "div#productMainContentContainerId div.ant-select.ant-select-outlined.css-1li46mu.ant-select-single.ant-select-show-arrow >> nth=1" # category_locator = "div#productMainContentContainerId div.ant-select.ant-select-outlined.css-1li46mu.ant-select-single.ant-select-show-arrow >> nth=0"
category_locator = "#productMainContentContainerId .ant-select.ant-select-outlined.css-1li46mu.ant-select-single.ant-select-show-arrow:nth-of-type(1)"
try: try:
await self.page.wait_for_selector(category_locator, timeout=5000) await self.page.wait_for_selector(category_locator, timeout=5000)

View File

@ -23,7 +23,6 @@ class TitleHandler:
self.category_main_selector_with_cp = self.locator_manager.get_locator('TitleLocators', 'category_main_selector_with_cp') self.category_main_selector_with_cp = self.locator_manager.get_locator('TitleLocators', 'category_main_selector_with_cp')
self.category_main_selector_with_ss = self.locator_manager.get_locator('TitleLocators', 'category_main_selector_with_ss') self.category_main_selector_with_ss = self.locator_manager.get_locator('TitleLocators', 'category_main_selector_with_ss')
self.category_main_selector_with_esm = self.locator_manager.get_locator('TitleLocators', 'category_main_selector_with_esm') self.category_main_selector_with_esm = self.locator_manager.get_locator('TitleLocators', 'category_main_selector_with_esm')
self.certified_text_locator = self.locator_manager.get_locator('TitleLocators', 'certified_text_locator')
self.category_text_locator = self.locator_manager.get_locator('TitleLocators', 'category_text_locator') self.category_text_locator = self.locator_manager.get_locator('TitleLocators', 'category_text_locator')
self.category_text_locator_certified = self.locator_manager.get_locator('TitleLocators', 'category_text_locator_certified') self.category_text_locator_certified = self.locator_manager.get_locator('TitleLocators', 'category_text_locator_certified')
@ -128,7 +127,7 @@ class TitleHandler:
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 get_category(self, market='ss') -> str: async def get_category_ori(self, market='ss') -> str:
""" """
카테고리를 가져오는 메서드로 인증 필요 여부에 따라 카테고리 선택자를 다르게 처리합니다. 카테고리를 가져오는 메서드로 인증 필요 여부에 따라 카테고리 선택자를 다르게 처리합니다.
@ -138,27 +137,27 @@ class TitleHandler:
try: try:
self.logger.debug(f"마켓 : {market} - 카테고리 텍스트를 가져오는 중입니다.") self.logger.debug(f"마켓 : {market} - 카테고리 텍스트를 가져오는 중입니다.")
if market == 'ss': if market == 'ss':
main_category_element = await self.page.query_selector(self.category_main_selector_with_ss) main_category_element = self.page.locator(self.category_main_selector_with_ss)
self.logger.debug(f"선택 마켓 : 스마트스토어 , selector : {self.category_main_selector_with_ss}, element : {main_category_element}") self.logger.debug(f"선택 마켓 : 스마트스토어 , selector : {self.category_main_selector_with_ss}, element : {main_category_element}")
elif market == 'cp': elif market == 'cp':
main_category_element = await self.page.query_selector(self.category_main_selector_with_cp) main_category_element = self.page.locator(self.category_main_selector_with_cp)
self.logger.debug(f"선택 마켓 : 쿠팡 , selector : {self.category_main_selector_with_cp}, element : {main_category_element}") self.logger.debug(f"선택 마켓 : 쿠팡 , selector : {self.category_main_selector_with_cp}, element : {main_category_element}")
elif market == 'esm': elif market == 'esm':
main_category_element = await self.page.query_selector(self.category_main_selector_with_esm) main_category_element = self.page.locator(self.category_main_selector_with_esm)
self.logger.debug(f"선택 마켓 : ESM , selector : {self.category_main_selector_with_esm}, element : {main_category_element}") self.logger.debug(f"선택 마켓 : ESM , selector : {self.category_main_selector_with_esm}, element : {main_category_element}")
if not main_category_element: if not main_category_element:
self.logger.error("카테고리 메인 선택자를 찾을 수 없습니다.") self.logger.error("카테고리 메인 선택자를 찾을 수 없습니다.")
return "" return ""
certified_text_element = await main_category_element.query_selector(self.certified_text_locator) certified_text_element = main_category_element.locator(self.certified_text_locator)
if certified_text_element: if certified_text_element:
certified_text = await certified_text_element.inner_text() certified_text = certified_text_element.inner_text()
if "인증" in certified_text: if "인증" in certified_text:
category_text_element = await main_category_element.query_selector(self.category_text_locator_certified) category_text_element = main_category_element.locator(self.category_text_locator_certified)
self.logger.debug(f"카테고리 인증 필요 발생: {category_text}") self.logger.debug(f"카테고리 인증 필요 발생: {category_text}")
else: else:
category_text_element = certified_text_element category_text_element = certified_text_element
category_text = await category_text_element.inner_text() if category_text_element else "" category_text = category_text_element.inner_text() if category_text_element else ""
self.logger.debug(f"카테고리 텍스트: {category_text}") self.logger.debug(f"카테고리 텍스트: {category_text}")
return category_text return category_text
else: else:
@ -167,3 +166,59 @@ class TitleHandler:
except Exception as e: except Exception as e:
self.logger.error(f"카테고리 텍스트 가져오기 중 오류 발생: {e}", exc_info=True) self.logger.error(f"카테고리 텍스트 가져오기 중 오류 발생: {e}", exc_info=True)
return "" return ""
async def get_category(self, market='ss') -> str:
"""
카테고리를 가져오는 메서드로 인증 필요 여부에 따라 카테고리 선택자를 다르게 처리합니다.
Returns:
str: 카테고리 텍스트
"""
try:
self.logger.debug(f"마켓 : {market} - 카테고리 텍스트를 가져오는 중입니다.")
if market == 'ss':
category_locator = self.category_main_selector_with_ss
elif market == 'cp':
category_locator = self.category_main_selector_with_cp
elif market == 'esm':
category_locator = self.category_main_selector_with_esm
self.logger.debug(f"category_locator : {category_locator}")
await self.page.wait_for_selector(category_locator, timeout=5000, state="attached") # 요소가 나타날 때까지 대기
main_category_element = self.page.locator(category_locator) # 대기 후 동기적으로 요소 가져오기
self.logger.debug(f"main_category_element : {main_category_element}")
if not await main_category_element.count():
self.logger.error("카테고리 메인 선택자를 찾을 수 없습니다.")
return ""
# 인증 텍스트 요소 선택
category_text_element = main_category_element.locator(self.category_text_locator)
self.logger.debug(f"category_text_element : {category_text_element}")
if await category_text_element.count():
category_text = await category_text_element.inner_text()
if "인증" in category_text:
self.logger.debug(f"카테고리 인증 필요 발생 category_text = {category_text}")
category_text_certified_element = main_category_element.locator(self.category_text_locator_certified)
if await category_text_certified_element.count():
category_text = await category_text_certified_element.inner_text()
self.logger.debug(f"인증 필요 카테고리 text = {category_text}")
else:
self.logger.debug(f"카테고리 text = {category_text}")
return category_text
else:
self.logger.error("카테고리 인증 요소를 찾을 수 없습니다.")
return ""
except Exception as e:
self.logger.error(f"카테고리 텍스트 가져오기 중 오류 발생: {e}", exc_info=True)
return ""