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

26
Todo.List Normal file
View File

@ -0,0 +1,26 @@
옵션번역을 위한 vertext API키 설정
옵션선택 기준 : 최대 옵션갯수, 옵션이미지번역유무, 옵션AI선택유무 , 옵션필터링 기준(표준편차범위등 ) 기본동작설명
가격조정 기준 : 카드수수료 설정, 기본마진 설정, 기본더하기마진과 해외배송비 설정, 적정가격 산출비율(팔린가격, 원가x2, ) 카테고리별 해외배송비 가중치 사용유무
더하기마진과, 해외배송비의 기본가중치 설정
상세페이지 기준 : 옵션명 입력 유무, 상세설명 유무 및 설정버튼, 상세페이지 이미지번역설정, 이미지번역방법(자체번역1(기본인페인팅+deepL), 자체번역2(AI인페인팅+deepL), 웨일번역VD모드, 웨일번역기본모드)
상품명 기준 : 네이버 키워드 상품정보 수집으로 자동키워드조합
형태소 분석을 활용한 키워드 조합
태그 기준 : 태그사용 유무
썸네일 기준 : 썸네일 번역기준
로거의 디버그와 인포 구분
유저설정을 db에 통합
프로그램 로그인연동
FastAPI연동
상품명 AI 작성
인자목록 : 원본상품명, 판매키워드, 네이버 상위판매상품의 상품명 20여개.등을 제공.
네이버 노출로직 제공하여 vertexai로 수행
상품명 프롬프트 "한글로 된 단어와 영어 또는 영어와 숫자로된 상품코드, 전압, 전류 또는 여러가지 스펙등을 인식할수 있고, 각 옵션별로 특징적인 내용만 남기는 방법을 사용하려면??
상품명에서 어느정도 옵션이 어떤내용을 말하는지를 유추할수 있잖니? 그래서 상품명과 옵션명들을 함께 제공할꺼야.
물론 중국어로 된 원문이야.
그래서 각 옵션별 특징을 간결하게 남긴 뒤 한글로 번역하는게 목표야."

Binary file not shown.

Binary file not shown.

Binary file not shown.

File diff suppressed because it is too large Load Diff

View File

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

View File

@ -5,26 +5,28 @@ exchange_fee_input_locator = '//*[@id='productMainContentContainerId']/div/div[1
plus_margin_locator = '//*[@id='productMainContentContainerId']/div/div[1]/div/div/div[2]/div/div[1]/div[8]/div/div/div[3]/div/div/div/div[1]/div[2]/input' plus_margin_locator = '//*[@id='productMainContentContainerId']/div/div[1]/div/div/div[2]/div/div[1]/div[8]/div/div/div[3]/div/div/div/div[1]/div[2]/input'
oversea_shipping_locator = '//*[@id='productMainContentContainerId']/div/div[1]/div/div/div[2]/div/div[1]/div[10]/div/div/div/div[1]/div[2]/input' oversea_shipping_locator = '//*[@id='productMainContentContainerId']/div/div[1]/div/div/div[2]/div/div[1]/div[10]/div/div/div/div[1]/div[2]/input'
option_count_text_locator = 'div#productMainContentContainerId th:nth-child(2) > div > span' option_count_text_locator = 'div#productMainContentContainerId th:nth-child(2) > div > span'
product_cost_locator = '//*[@id='productMainContentContainerId']/div/div[2]/div/div/div[5]/div[1]/div/div/div/div/div[2]/table/tbody/tr[{i}]/td[3]/div/div/div/div[2]/input' product_cost_locator = '//*[@id='productMainContentContainerId']/div/div[2]/div/div/div[5]/div[1]/div/div/div/div/div[2]/table/tbody/tr[{index}]/td[3]/div/div/div/div[2]/input'
standard_selling_price_locator = '//*[@id='productMainContentContainerId']/div/div[2]/div/div/div[5]/div[1]/div/div/div/div/div[2]/table/tbody/tr[{i}]/td[4]/div/div/div[1]/div/div[2]/input' standard_selling_price_locator = '//*[@id='productMainContentContainerId']/div/div[2]/div/div/div[5]/div[1]/div/div/div/div/div[2]/table/tbody/tr[{index}]/td[4]/div/div/div[1]/div/div[2]/input'
[OptionLocators] [OptionLocators]
# 옵션 관련 선택자 # 옵션 관련 선택자
option_excluded_selector_template = '//*[@id="productMainContentContainerId"]/div[1]/div[2]/div/div/div[2]/div/div[1]/div/div/div[2]/div/div/div[5]/div[1]/div/div/ul/li[{i}]/div/div[1]/div/div[2]/div/div[3]' option_excluded_selector_template = '//*[@id="productMainContentContainerId"]/div[1]/div[2]/div/div/div[2]/div/div[1]/div/div/div[2]/div/div/div[5]/div[1]/div/div/ul/li[{index}]/div/div[1]/div/div[2]/div/div[3]'
option_input_selector_template = '//*[@id="productMainContentContainerId"]/div[1]/div[2]/div/div/div[2]/div/div[1]/div/div/div[2]/div/div/div[5]/div[1]/div/div/ul/li[{i}]/div/div[1]/div/div[3]/div[2]/div[1]/span/input' option_input_selector_template = '//*[@id="productMainContentContainerId"]/div[1]/div[2]/div/div/div[2]/div/div[1]/div/div/div[2]/div/div/div[5]/div[1]/div/div/ul/li[{index}]/div/div[1]/div/div[3]/div[2]/div[1]/span/input'
single_option_locator = '//div[@id="productMainContentContainerId"]//label[contains(@class, 'ant-radio-button-wrapper-checked') and contains(., '단일 상품등록')]' single_option_locator = '//div[@id="productMainContentContainerId"]//label[contains(@class, 'ant-radio-button-wrapper-checked') and contains(., '단일 상품등록')]'
option_product_locator = '//div[@id="productMainContentContainerId"]//label[contains(@class, 'ant-radio-button-wrapper-checked') and contains(., '옵션 상품등록')]' option_product_locator = '//div[@id="productMainContentContainerId"]//label[contains(@class, 'ant-radio-button-wrapper-checked') and contains(., '옵션 상품등록')]'
total_options_selector = '#productMainContentContainerId label.ant-checkbox-wrapper' total_options_selector = '#productMainContentContainerId label.ant-checkbox-wrapper'
is_all_option_checked_selector = '#productMainContentContainerId .ant-checkbox-indeterminate' is_all_option_checked_selector = '#productMainContentContainerId .ant-checkbox-indeterminate'
original_name_selector_template = 'div#productMainContentContainerId li:nth-child({i}) > div > div:nth-child(1) > div > div:nth-child(3) > div:nth-child(3) > span' original_name_selector_template = 'div#productMainContentContainerId li:nth-child({index}) > div > div:nth-child(1) > div > div:nth-child(3) > div:nth-child(3) > span'
edit_field_selector_template = 'div#productMainContentContainerId li:nth-child({i}) > div > div:nth-child(1) > div > div:nth-child(3) > div:nth-child(2) > div:nth-child(1) > span > input' edit_field_selector_template = 'div#productMainContentContainerId li:nth-child({index}) > div > div:nth-child(1) > div > div:nth-child(3) > div:nth-child(2) > div:nth-child(1) > span > input'
checkbox_selector_template = '#productMainContentContainerId li:nth-child({i}) input[type="checkbox"]' checkbox_selector_template = '#productMainContentContainerId li:nth-child({index}) input[type="checkbox"]'
image_selector_template = '#productMainContentContainerId li:nth-child({i}) img.sc-gbvfcU.ezktkd' image_selector_template = '#productMainContentContainerId li:nth-child({index}) img.sc-gbvfcU.ezktkd'
price_selector_template = '#productMainContentContainerId li:nth-child({i}) sup' price_selector_template = '#productMainContentContainerId li:nth-child({index}) sup'
delete_button_selector_template = '#productMainContentContainerId > div.sc-TOgAA.fZvEqY > div:nth-child(2) > div > div > div:nth-child(2) > div > div.sc-cFShuL.dbIeho > div > div > div.ant-collapse-content.ant-collapse-content-active > div > div > div.sc-fGdiLE.iyXMeU > div.ant-list.ant-list-split.css-1li46mu > div > div > ul > li:nth-child({i}) > div > div:nth-child(1) > div > div:nth-child(2) > div > div.ant-row.ant-row-no-wrap.ant-row-space-between.ant-row-middle.css-1li46mu > div:nth-child(1) > div' delete_button_selector_template = '#productMainContentContainerId > div.sc-TOgAA.fZvEqY > div:nth-child(2) > div > div > div:nth-child(2) > div > div.sc-cFShuL.dbIeho > div > div > div.ant-collapse-content.ant-collapse-content-active > div > div > div.sc-fGdiLE.iyXMeU > div.ant-list.ant-list-split.css-1li46mu > div > div > ul > li:nth-child({index}) > div > div:nth-child(1) > div > div:nth-child(2) > div > div.ant-row.ant-row-no-wrap.ant-row-space-between.ant-row-middle.css-1li46mu > div:nth-child(1) > div'
confirm_delete_button_locator = 'body > div:nth-child(18) > div > div.ant-modal-wrap.ant-modal-confirm-centered.ant-modal-centered > div > div.sc-ddjGPC.jbwEYW > div > div > div > div.ant-modal-confirm-btns > button.ant-btn.css-1li46mu.ant-btn-primary.ant-btn-dangerous' confirm_delete_button_locator = 'body > div:nth-child(18) > div > div.ant-modal-wrap.ant-modal-confirm-centered.ant-modal-centered > div > div.sc-ddjGPC.jbwEYW > div > div > div > div.ant-modal-confirm-btns > button.ant-btn.css-1li46mu.ant-btn-primary.ant-btn-dangerous'
add_button_selector_template = '#productMainContentContainerId > div.sc-TOgAA.fZvEqY > div:nth-child(2) > div > div > div:nth-child(2) > div > div.sc-cFShuL.dbIeho > div > div > div.ant-collapse-content.ant-collapse-content-active > div > div > div.sc-fGdiLE.iyXMeU > div.ant-list.ant-list-split.css-1li46mu > div > div > ul > li:nth-child({i}) > div > div:nth-child(1) > div > div:nth-child(2) > div > div > img' add_button_selector_template = '#productMainContentContainerId > div.sc-TOgAA.fZvEqY > div:nth-child(2) > div > div > div:nth-child(2) > div > div.sc-cFShuL.dbIeho > div > div > div.ant-collapse-content.ant-collapse-content-active > div > div > div.sc-fGdiLE.iyXMeU > div.ant-list.ant-list-split.css-1li46mu > div > div > ul > li:nth-child({index}) > div > div:nth-child(1) > div > div:nth-child(2) > div > div > img'
file_input_locator = 'input[type="file"]' file_input_locator = 'input[type="file"]'
low_order_button_locator = 'button:has-text("가격 낮은 순")'
AtoZ_button_locator = 'button:has-text("A-Z")'
[DetailLocators] [DetailLocators]
product_detail_input_locator = '//*[@id='detailMainContainerId']/div/div/div[{i}]/textarea' product_detail_input_locator = '//*[@id='detailMainContainerId']/div/div/div[{i}]/textarea'

24
gui.py
View File

@ -10,9 +10,11 @@ from price import PriceHandler
from title import TitleHandler from title import TitleHandler
from locatorManager import LocatorManager from locatorManager import LocatorManager
from src.cmb_diag import CMBSettingsDialog from src.cmb_diag import CMBSettingsDialog
from src.DatabaseManager import DatabaseManager
from logger_module import QTextEditLogger # 추가 from logger_module import QTextEditLogger # 추가
import logging import logging
import asyncio import asyncio
import os, shutil
class TranslationApp(QWidget): class TranslationApp(QWidget):
def __init__(self, logger=None, app=None): def __init__(self, logger=None, app=None):
@ -32,7 +34,23 @@ class TranslationApp(QWidget):
self.vertexAI = VertexAITranslator(self.logger, key_path) self.vertexAI = VertexAITranslator(self.logger, key_path)
self.optionHandler = None self.optionHandler = None
self.cmb_diag = CMBSettingsDialog(parent=self, logger=self.logger, debug=True) # DB 파일 경로 설정
self.base_dir = os.path.dirname(os.path.abspath(__file__))
self.user_db_path = os.path.join(self.base_dir, "userDB.db")
self.initial_db_path = os.path.join(self.base_dir, "src", "initialDB.db")
# userDB.db 파일이 없으면 initialDB.db를 복사해서 생성
if not os.path.exists(self.user_db_path):
if os.path.exists(self.initial_db_path):
shutil.copyfile(self.initial_db_path, self.user_db_path)
print("initialDB.db를 userDB.db로 복사했습니다.")
else:
raise FileNotFoundError("initialDB.db 파일이 없습니다. 초기 DB 파일이 존재하는지 확인해주세요.")
# DatabaseManager 초기화
self.db_manager = DatabaseManager(db_url=f"sqlite:///{self.user_db_path}", logger=self.logger)
self.cmb_diag = CMBSettingsDialog(parent=self, logger=self.logger, db_manager=self.db_manager, initial_db_path=self.initial_db_path, user_db_path=self.user_db_path, debug=self.debug)
self.clipboardImageManager = ClipboardImageManager(self, logger, self.browser_controller, self.debug) self.clipboardImageManager = ClipboardImageManager(self, logger, self.browser_controller, self.debug)
self.optionHandler = OptionHandler(self.locator_manager, self.browser_controller, self.whale_translator, self.logger, self.vertexAI, self.debug) self.optionHandler = OptionHandler(self.locator_manager, self.browser_controller, self.whale_translator, self.logger, self.vertexAI, self.debug)
self.priceHandler = PriceHandler(self.locator_manager, self.browser_controller, self.logger, self.vertexAI, self.cmb_diag, self.debug) self.priceHandler = PriceHandler(self.locator_manager, self.browser_controller, self.logger, self.vertexAI, self.cmb_diag, self.debug)
@ -698,7 +716,7 @@ class TranslationApp(QWidget):
self.complete_stage(0) self.complete_stage(0)
if self.toggle_states['optionTrnas'] or self.toggle_states['optionIMGTrans'] or self.toggle_states['optionAutoSelect']: if self.toggle_states['optionTrnas'] or self.toggle_states['optionIMGTrans'] or self.toggle_states['optionAutoSelect']:
self.logger.debug(f"옵션수정 : {self.toggle_states['optionTrnas']} + {self.toggle_states['optionIMGTrans']} + {self.toggle_states['optionAutoSelect']}") self.logger.debug(f"옵션수정 : optionTrnas={self.toggle_states['optionTrnas']} + optionIMGTrans={self.toggle_states['optionIMGTrans']} + optionAutoSelect{self.toggle_states['optionAutoSelect']}")
# 옵션 수정 # 옵션 수정
self.start_stage(0) self.start_stage(0)
await self.edit_option(product_name) await self.edit_option(product_name)
@ -811,7 +829,7 @@ class TranslationApp(QWidget):
# 옵션 최대선택갯수 # 옵션 최대선택갯수
max_option_count = 20 max_option_count = 20
option_image_trans = False option_image_trans = False
await self.optionHandler.process_options(product_name, max_option_count, option_image_trans) await self.optionHandler.process_options(product_name, max_option_count, self.toggle_states)
# 수정 후 저장 # 수정 후 저장
# await self.optionHandler.save_option() # await self.optionHandler.save_option()

View File

@ -73,6 +73,8 @@ class LocatorManager:
'confirm_delete_button_locator': self.config.get('OptionLocators', 'confirm_delete_button_locator').strip("'"), 'confirm_delete_button_locator': self.config.get('OptionLocators', 'confirm_delete_button_locator').strip("'"),
'add_button_selector_template': self.config.get('OptionLocators', 'add_button_selector_template').strip("'"), 'add_button_selector_template': self.config.get('OptionLocators', 'add_button_selector_template').strip("'"),
'file_input_locator': self.config.get('OptionLocators', 'file_input_locator').strip("'"), 'file_input_locator': self.config.get('OptionLocators', 'file_input_locator').strip("'"),
'low_order_button_locator': self.config.get('OptionLocators', 'low_order_button_locator').strip("'"),
'AtoZ_button_locator': self.config.get('OptionLocators', 'AtoZ_button_locator').strip("'"),
} }
# TitleLocators 섹션 # TitleLocators 섹션

View File

@ -2,7 +2,7 @@ from collections import Counter
import pyautogui import pyautogui
from datetime import datetime from datetime import datetime
import numpy as np import numpy as np
import asyncio import asyncio, time, math
class OptionHandler: class OptionHandler:
def __init__(self, locator_manager, browser_controller, whale_translator, logger, vertexAI, debug_flag=False): def __init__(self, locator_manager, browser_controller, whale_translator, logger, vertexAI, debug_flag=False):
@ -31,6 +31,8 @@ class OptionHandler:
self.confirm_delete_button_locator = self.locator_manager.get_locator('OptionLocators', 'confirm_delete_button_locator') self.confirm_delete_button_locator = self.locator_manager.get_locator('OptionLocators', 'confirm_delete_button_locator')
self.add_button_selector_template = self.locator_manager.get_locator('OptionLocators', 'add_button_selector_template') self.add_button_selector_template = self.locator_manager.get_locator('OptionLocators', 'add_button_selector_template')
self.file_input_locator = self.locator_manager.get_locator('OptionLocators', 'file_input_locator') self.file_input_locator = self.locator_manager.get_locator('OptionLocators', 'file_input_locator')
self.low_order_button_locator = self.locator_manager.get_locator('OptionLocators', 'low_order_button_locator')
self.AtoZ_button_locator = self.locator_manager.get_locator('OptionLocators', 'AtoZ_button_locator')
def update_page(self, page1): def update_page(self, page1):
self.page = page1 self.page = page1
@ -82,6 +84,11 @@ class OptionHandler:
self.logger.debug(f"최저옵션: {mean_price}, 표준편차: {std_price}") self.logger.debug(f"최저옵션: {mean_price}, 표준편차: {std_price}")
if std_price == 0:
# 모든 옵션의 가격이 동일한 경우 그대로 반환
self.logger.debug("모든 옵션의 가격이 동일합니다. 필터링 없이 모든 옵션을 반환합니다.")
return options
filtered_options = [] filtered_options = []
for option in options: for option in options:
z_score = (option['price'] - mean_price) / std_price z_score = (option['price'] - mean_price) / std_price
@ -102,8 +109,7 @@ class OptionHandler:
# 최종적으로 필터링된 가격 범위 내에 있는 옵션만 선택 # 최종적으로 필터링된 가격 범위 내에 있는 옵션만 선택
final_options = [option for option in filtered_options if lower_bound <= option['price'] <= upper_bound] final_options = [option for option in filtered_options if lower_bound <= option['price'] <= upper_bound]
self.logger.debug(f"최종 선택된 옵션: {[opt['price'] for opt in final_options]}") self.logger.debug(f"최종 선택된 옵션: {[(opt['name'], opt['price']) for opt in final_options]}")
return final_options return final_options
async def store_selected_options(self): async def store_selected_options(self):
@ -111,13 +117,13 @@ class OptionHandler:
try: try:
selected_translated_options = [] selected_translated_options = []
total_options_count = len(self.option_info['original_names']) total_options_count = len([self.option_info]['original_names'])
await self.low_order_click() await self.low_order_click()
for i in range(1, total_options_count + 1): for i in range(1, total_options_count + 1):
option_excluded_selector = self.option_excluded_selector_template.format(i) option_excluded_selector = self.option_excluded_selector_template.format(index=i)
option_input_selector = self.option_input_selector_template.format(i) option_input_selector = self.option_input_selector_template.format(index=i)
option_excluded_element = await self.page.query_selector(option_excluded_selector) option_excluded_element = await self.page.query_selector(option_excluded_selector)
if not option_excluded_element: if not option_excluded_element:
@ -135,7 +141,7 @@ class OptionHandler:
self.logger.error(f"선택된 옵션 저장 중 오류 발생: {e}", exc_info=True) self.logger.error(f"선택된 옵션 저장 중 오류 발생: {e}", exc_info=True)
async def process_options(self, product_name, max_option_count=20, option_image_trans_flag=False): async def process_options(self, product_name, max_option_count=20, toggle_states=False):
""" """
옵션 처리 로직. 옵션을 번역하고 이미지를 업데이트함. 옵션 처리 로직. 옵션을 번역하고 이미지를 업데이트함.
@ -171,6 +177,8 @@ class OptionHandler:
await self.low_order_click() await self.low_order_click()
# 4. 옵션 정보 수집 및 번역 # 4. 옵션 정보 수집 및 번역
if toggle_states['optionTrnas']:
self.logger.debug(f"옵션 AI번역 : {toggle_states['optionTrnas']}")
self.option_info = await self.collect_options_info() self.option_info = await self.collect_options_info()
translation_success = False # 성공/실패 플래그 translation_success = False # 성공/실패 플래그
@ -188,10 +196,16 @@ class OptionHandler:
self.logger.error(f"안전 필터에 의해 번역 요청이 차단되었습니다. {ve}") self.logger.error(f"안전 필터에 의해 번역 요청이 차단되었습니다. {ve}")
self.logger.debug(f"퍼센티 자체 AI번역 사용 시도") self.logger.debug(f"퍼센티 자체 AI번역 사용 시도")
pyautogui.hotkey('alt', 'q') pyautogui.hotkey('alt', 'q')
self.logger.debug(f"번역을 위한 5초간 대기")
# await asyncio.sleep(5)
time.sleep(5)
translation_success = False # 번역 실패 translation_success = False # 번역 실패
self.logger.debug(f"[{'VertexAI' if translation_success else '퍼센티AI'}] 를 이용한 옵션번역 성공")
# 5. 옵션 필터링 및 조정 # 5. 옵션 필터링 및 조정
self.logger.debug(f"옵션 필터링 및 조정") if toggle_states['optionAutoSelect']:
self.logger.debug(f"옵션 필터링 및 조정 : {toggle_states['optionAutoSelect']}")
await self.filter_and_adjust_options(max_option_count) await self.filter_and_adjust_options(max_option_count)
# 6. 선택된 옵션 재수집 # 6. 선택된 옵션 재수집
@ -199,8 +213,8 @@ class OptionHandler:
await self.store_selected_options() # 페이지에서 실제 선택된 옵션을 수집하여 저장 await self.store_selected_options() # 페이지에서 실제 선택된 옵션을 수집하여 저장
# 7. 옵션 이미지 업데이트 (옵션 이미지가 있는 경우) # 7. 옵션 이미지 업데이트 (옵션 이미지가 있는 경우)
if option_image_trans_flag: if toggle_states['optionIMGTrans']:
self.logger.debug("옵션 이미지 업데이트 (옵션 이미지가 있는 경우만)") self.logger.debug(f"옵션 이미지번역(옵션 이미지가 있는 경우만) : {toggle_states['optionIMGTrans']}")
for index, option_image_url in enumerate(self.option_info.get('option_images', []), start=1): for index, option_image_url in enumerate(self.option_info.get('option_images', []), start=1):
option_name = translated_options.get(f'trans_option_{index}', f'옵션_{index}') option_name = translated_options.get(f'trans_option_{index}', f'옵션_{index}')
await self.update_option_image(index, option_image_url, product_name, option_name, self.debug_flag) await self.update_option_image(index, option_image_url, product_name, option_name, self.debug_flag)
@ -283,11 +297,11 @@ class OptionHandler:
# 옵션 정보를 비동기로 수집 (각 항목 병렬 처리) # 옵션 정보를 비동기로 수집 (각 항목 병렬 처리)
for i in range(1, total_options_count + 1): for i in range(1, total_options_count + 1):
try: try:
original_name_selector = self.original_name_selector_template.format(i) original_name_selector = self.original_name_selector_template.format(index=i)
edit_field_selector = self.edit_field_selector_template.format(i) edit_field_selector = self.edit_field_selector_template.format(index=i)
checkbox_selector = self.checkbox_selector_template.format(i) checkbox_selector = self.checkbox_selector_template.format(index=i)
image_selector = self.image_selector_template.format(i) image_selector = self.image_selector_template.format(index=i)
price_selector = self.price_selector_template.format(i) price_selector = self.price_selector_template.format(index=i)
tasks = [ tasks = [
self.page.query_selector(original_name_selector), self.page.query_selector(original_name_selector),
@ -435,15 +449,15 @@ class OptionHandler:
if edit_field: if edit_field:
await edit_field.fill(translated_name) # 필드에 번역된 옵션명 입력 await edit_field.fill(translated_name) # 필드에 번역된 옵션명 입력
self.logger.debug(f"{key}번째 translated_name : [{translated_name}] 입력 완료") self.logger.info(f"{key}번째 translated_name : [{translated_name}] 입력 완료")
# 업데이트할 번역된 이름을 임시 딕셔너리에 저장 # 업데이트할 번역된 이름을 임시 딕셔너리에 저장
updated_translations[original_name] = translated_name updated_translations[original_name] = translated_name
else: else:
self.logger.debug(f"{key}번째 옵션 필드가 없습니다.") self.logger.error(f"{key}번째 옵션 필드가 없습니다.")
else: else:
self.logger.debug(f"원본 옵션명을 찾을 수 없습니다: {origin_option_key}") self.logger.error(f"원본 옵션명을 찾을 수 없습니다: {origin_option_key}")
# 모든 번역이 끝난 후 한 번에 업데이트 # 모든 번역이 끝난 후 한 번에 업데이트
self.option_info['selected_translated_options'].update(updated_translations) self.option_info['selected_translated_options'].update(updated_translations)
@ -452,6 +466,20 @@ class OptionHandler:
except Exception as e: except Exception as e:
self.logger.error(f"번역된 옵션명을 입력하는 중 오류 발생: {e}", exc_info=True) self.logger.error(f"번역된 옵션명을 입력하는 중 오류 발생: {e}", exc_info=True)
def round_to_UP(self, number, nearest=1000):
"""
숫자를 주어진 'nearest' 값에 맞춰 올림합니다.
Parameters:
- number (float or int): 올림할 숫자
- nearest (int): 올림할 단위 (기본값 1000)
Returns:
- rounded_number (int): 올림된 숫자
"""
# nearest 값으로 나눈 후 math.ceil을 사용하여 올림
rounded_number = math.ceil(number / nearest) * nearest
return rounded_number
async def filter_and_adjust_options(self, max_option_count): async def filter_and_adjust_options(self, max_option_count):
"""가격 필터링을 적용하고 옵션을 조정""" """가격 필터링을 적용하고 옵션을 조정"""
@ -466,7 +494,7 @@ class OptionHandler:
options_list = [ options_list = [
{ {
"name": name, "name": name,
"price": (info['low_price'] + info['high_price']) / 2 # 중간 값을 사용하여 필터링 "price": self.round_to_UP((info['low_price'] + info['high_price']) / 2) # 중간 값을 사용하여 필터링
} }
for name, info in prices.items() for name, info in prices.items()
] ]
@ -501,43 +529,41 @@ class OptionHandler:
try: try:
# 옵션 체크 상태를 수집한 정보에서 필터링된 옵션들만 체크 상태로 유지 # 옵션 체크 상태를 수집한 정보에서 필터링된 옵션들만 체크 상태로 유지
for i, name in enumerate(self.option_info['original_names'].values()): for i, name in enumerate(self.option_info['original_names'].values()):
checkbox_selector = self.checkbox_selector_template.format(i+1) checkbox_selector = self.checkbox_selector_template.format(index=i+1)
checkbox_element = await self.page.query_selector(checkbox_selector) checkbox_element = await self.page.query_selector(checkbox_selector)
is_checked = self.option_info['checked_states'].get(name, False) is_checked = self.option_info['checked_states'].get(name, False)
# 디버깅 로그: 현재 옵션 이름과 필터링된 옵션 이름 확인
self.logger.debug(f"옵션 이름: {name}, 필터링된 옵션에 포함 여부: {name in filtered_option_names}, 현재 체크 상태: {is_checked}")
if checkbox_element: if checkbox_element:
# 필터링된 이름에 포함되는 경우
if name in filtered_option_names: if name in filtered_option_names:
# 체크되어 있지 않으면 체크 수행
if not is_checked: if not is_checked:
await checkbox_element.click() await checkbox_element.click()
self.logger.debug(f"옵션 '{name}' 체크함")
self.option_info['checked_states'][name] = True self.option_info['checked_states'][name] = True
# 필터링된 이름에 포함되지 않는 경우
else: else:
# 체크되어 있으면 체크 해제 수행
if is_checked: if is_checked:
await checkbox_element.click() await checkbox_element.click()
self.logger.debug(f"옵션 '{name}' 체크 해제함")
self.option_info['checked_states'][name] = False self.option_info['checked_states'][name] = False
self.logger.debug(f"옵션 체크 상태 조정 완료.") self.logger.debug(f"옵션 체크 상태 조정 완료.")
except Exception as e: except Exception as e:
self.logger.error(f"옵션 체크 상태 조정 중 오류 발생: {e}", exc_info=True) self.logger.error(f"옵션 체크 상태 조정 중 오류 발생: {e}", exc_info=True)
async def AtoZ_button_click(self): async def AtoZ_button_click(self):
AtoZ_button_locator = self.locator_manager.get_locator('OptionLocators', 'AtoZ_button_locator')
self.logger.debug("A-Z 버튼을 클릭합니다.") self.logger.debug("A-Z 버튼을 클릭합니다.")
await self.page.click(AtoZ_button_locator) await self.page.click(self.AtoZ_button_locator)
async def low_order_click(self): async def low_order_click(self):
low_order_button_locator = self.locator_manager.get_locator('OptionLocators', 'low_order_button_locator')
self.logger.debug("가격 낮은 순 정렬을 클릭합니다.") self.logger.debug("가격 낮은 순 정렬을 클릭합니다.")
await self.page.click(low_order_button_locator) await self.page.click(self.low_order_button_locator)
async def save_option(self):
"""옵션 수정 후 저장 버튼 클릭"""
save_button_locator = self.locator_manager.get_locator('OptionLocators', 'save_button_locator')
try:
await self.page.click(save_button_locator)
self.logger.debug("옵션 수정 내용 저장 완료.")
except Exception as e:
self.logger.debug(f"옵션수정 후 저장 버튼 클릭 중 오류: {e}", exc_info=True)
async def update_option_image(self, index, option_image_url, product_name, option_name, debug_flag=False): async def update_option_image(self, index, option_image_url, product_name, option_name, debug_flag=False):
""" """
@ -557,9 +583,9 @@ class OptionHandler:
self.logger.debug(f"{index}번째 옵션의 이미지를 업데이트합니다.") self.logger.debug(f"{index}번째 옵션의 이미지를 업데이트합니다.")
delete_button_selector = self.delete_button_selector_template.format(index) delete_button_selector = self.delete_button_selector_template.format(index=index)
confirm_delete_button_locator = self.confirm_delete_button_locator confirm_delete_button_locator = self.confirm_delete_button_locator
add_button_selector = self.add_button_selector_template.format(index) add_button_selector = self.add_button_selector_template.format(index=index)
file_input_locator = self.file_input_locator file_input_locator = self.file_input_locator
# 기존 이미지 삭제 (삭제 버튼이 존재할 경우) # 기존 이미지 삭제 (삭제 버튼이 존재할 경우)

117
option_translator.py Normal file
View File

@ -0,0 +1,117 @@
import re
import jieba
from collections import Counter
from transformers import pipeline
class ProductOptionTranslator:
def __init__(self, product_name, options, translator):
"""
옵션 명칭을 간소화하고 번역하는 클래스.
Parameters:
- product_name (str): 상품명
- options (list): 옵션명 리스트 (중국어 원문)
- translator (object): 번역기 객체 (: transformers의 pipeline )
"""
self.product_name = product_name
self.options = options
self.translator = translator
def simplify_options(self):
"""
옵션의 특징을 간결하게 남기고 불필요한 정보를 제거합니다.
Returns:
- simplified_options (list): 간소화된 옵션명 리스트
"""
# 상품명에서 공통적인 키워드 추출
common_words = self.extract_common_words()
simplified_options = []
for option in self.options:
# 형태소 분석을 통해 단어 단위로 분리
words = list(jieba.cut(option))
# 의미 있는 단어만 남기기 (숫자, 영어, 특수한 패턴 유지)
keywords = [word for word in words if self.is_significant(word, common_words)]
# 간결하게 옵션명 재구성
simplified_option = " ".join(keywords)
simplified_options.append(simplified_option)
return simplified_options
def extract_common_words(self):
"""
상품명에서 공통적으로 사용되는 키워드를 추출합니다.
Returns:
- common_words (set): 상품명에서 중복되는 일반적인 단어들의 집합
"""
product_words = list(jieba.cut(self.product_name))
word_counts = Counter(product_words)
common_words = {word for word, count in word_counts.items() if count > 1}
return common_words
def is_significant(self, word, common_words):
"""
단어가 의미 있는지 확인합니다. 의미 없는 수식어나 중복된 단어는 제거합니다.
Parameters:
- word (str): 검사할 단어
- common_words (set): 상품명에서 추출한 공통 단어들
Returns:
- (bool): 단어가 의미 있는 경우 True, 그렇지 않은 경우 False
"""
# 공통 단어 또는 의미 없는 수식어는 제거
if word in common_words:
return False
# 숫자, 영어 또는 특정 패턴(전압, 전류 등)은 의미 있는 단어로 간주
if re.match(r'[A-Za-z0-9]+', word) or re.match(r'\d+(V|A|W|Hz)', word):
return True
return True if len(word) > 1 else False
def translate_options(self, simplified_options):
"""
간소화된 옵션명을 한글로 번역합니다.
Parameters:
- simplified_options (list): 간소화된 옵션명 리스트
Returns:
- translated_options (list): 한글로 번역된 옵션명 리스트 (25 이내로 축약)
"""
translated_options = []
for option in simplified_options:
translated_text = self.translator(option, src_lang="zh", tgt_lang="ko")
# 번역된 결과가 25자를 넘을 경우 축약
if len(translated_text) > 25:
translated_text = translated_text[:25] + '...'
translated_options.append(translated_text)
return translated_options
def process_options(self):
"""
전체 옵션 처리 과정을 수행합니다 (간소화 + 번역).
Returns:
- translated_options (list): 최종 한글로 번역된 옵션 리스트
"""
simplified_options = self.simplify_options()
return self.translate_options(simplified_options)
# 번역기 예시 (transformers의 pipeline을 사용한 경우)
translator = pipeline('translation', model='Helsinki-NLP/opus-mt-zh-ko')
# 예제 데이터
product_name = "전기 드릴 18V 가정용"
options = ["18V 2.0Ah 배터리 포함", "18V 본체만", "충전기 포함 세트", "전용 케이스"]
# 클래스 사용
translator_class = ProductOptionTranslator(product_name, options, translator)
translated_options = translator_class.process_options()
for idx, translated_option in enumerate(translated_options, 1):
print(f"옵션 {idx}: {translated_option}")

View File

@ -574,8 +574,8 @@ class PriceHandler:
# 옵션 개수만큼 순회하면서 값을 수집 # 옵션 개수만큼 순회하면서 값을 수집
for i in range(1, total_options + 1): for i in range(1, total_options + 1):
product_cost_locator = self.product_cost_locator_template.format(i=i+1) product_cost_locator = self.product_cost_locator_template.format(index=i+1)
standard_selling_price_locator = self.standard_selling_price_locator_template.format(i=i+1) standard_selling_price_locator = self.standard_selling_price_locator_template.format(index=i+1)
# 각 선택자를 사용하여 요소 찾기 # 각 선택자를 사용하여 요소 찾기
product_cost_element = await self.page.wait_for_selector(product_cost_locator) product_cost_element = await self.page.wait_for_selector(product_cost_locator)
@ -749,27 +749,12 @@ class PriceHandler:
""" """
total_extra_shipping = 0 total_extra_shipping = 0
self.logger.debug(f"category : {category}") self.logger.debug(f"category : {category}")
cmb_config = self.cmb_diag.get_crmobi_stage(category)
# locator_manager에서 카테고리별 해외배송비 설정을 불러옴 # cmb_diag에서 카테고리별 해외배송비 설정을 불러옴
category_data = self.locator_manager.get_category_data(category) category_data = self.cmb_diag.get_crmobi_stage(category)
if category_data: # 카테고리 설정이 있는지 확인 if category_data: # 카테고리 설정이 있는지 확인
threshold_index = 1 threshold, unit, extra_shipping = category_data
# threshold와 extra_shipping의 dynamic한 처리
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: if product_price > threshold:
excess_amount = product_price - threshold excess_amount = product_price - threshold
@ -777,7 +762,6 @@ class PriceHandler:
total_extra_shipping += increments * extra_shipping total_extra_shipping += increments * extra_shipping
self.logger.debug(f"{category} 카테고리, threshold {threshold}을 초과하여 추가 해외배송비 {increments * extra_shipping} 추가 적용") self.logger.debug(f"{category} 카테고리, threshold {threshold}을 초과하여 추가 해외배송비 {increments * extra_shipping} 추가 적용")
threshold_index += 1 # 다음 구간으로 이동
else: else:
self.logger.debug(f"카테고리 {category}는 추가 해외배송비 규칙이 없습니다. 추가 해외배송비는 0으로 설정됩니다.") self.logger.debug(f"카테고리 {category}는 추가 해외배송비 규칙이 없습니다. 추가 해외배송비는 0으로 설정됩니다.")

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

View File

@ -2,30 +2,39 @@ from PySide6.QtWidgets import (QDialog, QFileDialog, QVBoxLayout, QHBoxLayout, Q
QSpinBox, QGroupBox, QFileDialog, QMessageBox, QComboBox) QSpinBox, QGroupBox, QFileDialog, QMessageBox, QComboBox)
from PySide6.QtCore import Qt from PySide6.QtCore import Qt
from PySide6.QtGui import QFont, QColor from PySide6.QtGui import QFont, QColor
import sqlite3 from src.DatabaseManager import DatabaseManager
# import sqlite3
import shutil import shutil
import os, re import os, re
import pandas as pd import pandas as pd
class CMBSettingsDialog(QDialog): class CMBSettingsDialog(QDialog):
def __init__(self, parent=None, logger=None, debug=False): def __init__(self, parent=None, logger=None, db_manager: DatabaseManager = None, initial_db_path = None, user_db_path = None, debug=False):
super().__init__(parent) super().__init__(parent)
self.setWindowTitle("CMB 설정") self.setWindowTitle("CMB 설정")
self.resize(900, 600) self.resize(900, 600)
self.db_manager = db_manager
self.initial_db_path = initial_db_path
self.user_db_path = user_db_path
self.logger = logger self.logger = logger
self.debug = debug self.debug = debug
# DB 파일 경로 설정 # DB 파일 경로 설정
self.user_db_path = os.path.join("src", "userDB.db") # self.user_db_path = os.path.join("src", "userDB.db")
self.initial_db_path = os.path.join("src", "initialDB.db") # self.initial_db_path = os.path.join("src", "initialDB.db")
# userDB 체크 및 초기화 # # userDB 체크 및 초기화
if not os.path.exists(self.user_db_path): # if not os.path.exists(self.user_db_path):
shutil.copyfile(self.initial_db_path, self.user_db_path) # shutil.copyfile(self.initial_db_path, self.user_db_path)
# # DB 연결 설정
# self.conn = sqlite3.connect(self.user_db_path)
# self.cursor = self.conn.cursor()
# DB 매니저가 제공되지 않으면 오류 발생
if self.db_manager is None:
raise ValueError("DatabaseManager 인스턴스가 필요합니다.")
# DB 연결 설정
self.conn = sqlite3.connect(self.user_db_path)
self.cursor = self.conn.cursor()
# 메인 레이아웃 설정 # 메인 레이아웃 설정
main_layout = QHBoxLayout() main_layout = QHBoxLayout()
@ -46,18 +55,19 @@ 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,0,1,1,3) search_layout.addWidget(self.search_input,0,1,1,8)
search_layout.addWidget(self.search_btn,0,2,1,1) search_layout.addWidget(self.search_btn,0,9,1,1)
self.cmb_view_btn = QPushButton("모두 보기") self.cmb_view_btn = QPushButton("CMB-ALL")
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,0,3,1,1) search_layout.addWidget(self.cmb_view_btn,0,10,1,1)
# 1레벨, 2레벨, 3레벨 콤보박스와 라벨 설정 # 1레벨, 2레벨, 3레벨 콤보박스와 라벨 설정
self.category_combo_label = QLabel("카테고리")
self.level1_combo = QComboBox() self.level1_combo = QComboBox()
self.level2_combo = QComboBox() self.level2_combo = QComboBox()
self.level3_combo = QComboBox() self.level3_combo = QComboBox()
self.reset_combo_btn = QPushButton("콤보리셋") self.reset_combo_btn = QPushButton("Reset")
# 기본적으로 "모두 보기" 옵션 추가 # 기본적으로 "모두 보기" 옵션 추가
self.level1_combo.addItem("모두 보기") self.level1_combo.addItem("모두 보기")
@ -66,13 +76,13 @@ class CMBSettingsDialog(QDialog):
# 레벨 필터링 라벨 추가 # 레벨 필터링 라벨 추가
# search_layout.addWidget(QLabel("1레벨:")) # search_layout.addWidget(QLabel("1레벨:"))
search_layout.addWidget(self.level1_combo,1,1,1,2) search_layout.addWidget(self.category_combo_label,1,0,1,1)
search_layout.addWidget(self.level1_combo,1,1,1,3)
# search_layout.addWidget(QLabel("2레벨:")) # search_layout.addWidget(QLabel("2레벨:"))
search_layout.addWidget(self.level2_combo,1,2,1,2) search_layout.addWidget(self.level2_combo,1,4,1,3)
# search_layout.addWidget(QLabel("3레벨:")) # search_layout.addWidget(QLabel("3레벨:"))
search_layout.addWidget(self.level3_combo,1,3,1,2) search_layout.addWidget(self.level3_combo,1,7,1,3)
search_layout.addWidget(self.level3_combo,1,3,1,2) search_layout.addWidget(self.reset_combo_btn,1,10,1,1)
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.update_level2_combo)
@ -243,47 +253,74 @@ class CMBSettingsDialog(QDialog):
def load_level1_categories(self): def load_level1_categories(self):
"""1레벨 카테고리를 DB에서 로드하여 콤보박스에 추가""" """1레벨 카테고리를 DB에서 로드하여 콤보박스에 추가"""
self.logger.debug("1레벨 카테고리를 업데이트")
try:
query = "SELECT DISTINCT category1 FROM categories WHERE category1 IS NOT NULL" query = "SELECT DISTINCT category1 FROM categories WHERE category1 IS NOT NULL"
self.cursor.execute(query) rows = self.db_manager.fetchall(query)
rows = self.cursor.fetchall()
for row in rows: for row in rows:
self.level1_combo.addItem(row[0]) self.level1_combo.addItem(row[0])
except Exception as e:
QMessageBox.critical(self, "DB Loading 오류", f"1레벨 카테고리를 업데이트 중 오류가 발생했습니다: {e}")
self.logger.error(f"1레벨 카테고리를 업데이트 중 오류 발생: {e}", exc_info=True)
def update_level2_combo(self): def update_level2_combo(self):
"""1레벨 선택 시, 2레벨 콤보박스를 업데이트""" """1레벨 선택 시, 2레벨 콤보박스를 업데이트"""
self.logger.debug("2레벨 카테고리를 업데이트")
try:
selected_level1 = self.level1_combo.currentText() selected_level1 = self.level1_combo.currentText()
self.level2_combo.clear() self.level2_combo.clear()
self.level2_combo.addItem("모두 보기") self.level2_combo.addItem("모두 보기")
# "모두 보기"가 선택된 경우에는 필터링하지 않음
if selected_level1 == "모두 보기": if selected_level1 == "모두 보기":
return return
query = "SELECT DISTINCT category2 FROM categories WHERE category1 = ? AND category2 IS NOT NULL" # selected_level1이 유효한 값일 경우에만 쿼리를 실행
self.cursor.execute(query, (selected_level1,)) if selected_level1:
rows = self.cursor.fetchall() query = "SELECT DISTINCT category2 FROM categories WHERE category1 = :level1 AND category2 IS NOT NULL"
rows = self.db_manager.fetchall(query, {"level1": selected_level1})
if rows is None:
self.logger.error("DB에서 데이터를 가져오지 못했습니다. 쿼리를 확인해주세요.")
return
for row in rows: for row in rows:
self.level2_combo.addItem(row[0]) self.level2_combo.addItem(row[0])
except Exception as e:
QMessageBox.critical(self, "DB Loading 오류", f"2레벨 카테고리를 업데이트 중 오류가 발생했습니다: {e}")
self.logger.error(f"2레벨 카테고리를 업데이트 중 오류 발생: {e}", exc_info=True)
def update_level3_combo(self): def update_level3_combo(self):
"""2레벨 선택 시, 3레벨 콤보박스를 업데이트""" """2레벨 선택 시, 3레벨 콤보박스를 업데이트"""
self.logger.debug("3레벨 카테고리를 업데이트")
try:
selected_level1 = self.level1_combo.currentText() selected_level1 = self.level1_combo.currentText()
selected_level2 = self.level2_combo.currentText() selected_level2 = self.level2_combo.currentText()
self.level3_combo.clear() self.level3_combo.clear()
self.level3_combo.addItem("모두 보기") self.level3_combo.addItem("모두 보기")
if selected_level2 == "모두 보기": # 파라미터를 제대로 전달하여 쿼리를 수행
return if selected_level1 != "모두 보기":
query = "SELECT DISTINCT category3 FROM categories WHERE category1 = :level1 AND category2 = :level2 AND category3 IS NOT NULL"
rows = self.db_manager.fetchall(query, {"level1": selected_level1, "level2": selected_level2})
else:
query = "SELECT DISTINCT category3 FROM categories WHERE category2 = :level2 AND category3 IS NOT NULL"
rows = self.db_manager.fetchall(query, {"level2": selected_level2})
query = "SELECT DISTINCT category3 FROM categories WHERE category1 = ? AND category2 = ? AND category3 IS NOT NULL" if rows is None:
self.cursor.execute(query, (selected_level1, selected_level2)) self.logger.error("DB에서 데이터를 가져오지 못했습니다. 쿼리를 확인해주세요.")
rows = self.cursor.fetchall() return
for row in rows: for row in rows:
self.level3_combo.addItem(row[0]) self.level3_combo.addItem(row[0])
except Exception as e:
QMessageBox.critical(self, "DB Loading 오류", f"3레벨 카테고리를 업데이트 중 오류가 발생했습니다: {e}")
self.logger.error(f"3레벨 카테고리를 업데이트 중 오류 발생: {e}", exc_info=True)
def reset_comboboxes(self): def reset_comboboxes(self):
self.logger.debug("1레벨, 2레벨, 3레벨 카테고리를 초기화")
try:
"""1레벨, 2레벨, 3레벨 콤보박스를 초기화하고 QTreeWidget을 전체 항목으로 필터링합니다.""" """1레벨, 2레벨, 3레벨 콤보박스를 초기화하고 QTreeWidget을 전체 항목으로 필터링합니다."""
# 각 레벨 콤보박스 초기화 # 각 레벨 콤보박스 초기화
self.level1_combo.setCurrentIndex(0) self.level1_combo.setCurrentIndex(0)
@ -292,8 +329,13 @@ class CMBSettingsDialog(QDialog):
# QTreeWidget 필터링 초기화 (모든 항목 표시) # QTreeWidget 필터링 초기화 (모든 항목 표시)
self.load_db_to_table() # 이 메서드는 전체 데이터를 다시 로드하는 메서드입니다. self.load_db_to_table() # 이 메서드는 전체 데이터를 다시 로드하는 메서드입니다.
except Exception as e:
QMessageBox.critical(self, "DB Loading 오류", f"1레벨, 2레벨, 3레벨 카테고리를 초기화 중 오류가 발생했습니다: {e}")
self.logger.error(f"1레벨, 2레벨, 3레벨 카테고리를 초기화 중 오류 발생: {e}", exc_info=True)
def filter_category_tree(self): def filter_category_tree(self):
self.logger.debug(f"레벨별 카테고리를 기준으로 트리뷰를 필터링")
try:
"""선택된 1레벨, 2레벨, 3레벨 카테고리를 기준으로 트리뷰를 필터링""" """선택된 1레벨, 2레벨, 3레벨 카테고리를 기준으로 트리뷰를 필터링"""
selected_level1 = self.level1_combo.currentText() selected_level1 = self.level1_combo.currentText()
selected_level2 = self.level2_combo.currentText() selected_level2 = self.level2_combo.currentText()
@ -301,21 +343,19 @@ class CMBSettingsDialog(QDialog):
# 기본 쿼리와 조건을 설정 # 기본 쿼리와 조건을 설정
query = '''SELECT id, 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 = {}
# 선택된 값에 따라 필터 추가
if selected_level1 != "모두 보기": if selected_level1 != "모두 보기":
query += " AND category1 = ?" query += " AND category1 = :level1"
args.append(selected_level1) args["level1"] = selected_level1
if selected_level2 != "모두 보기": if selected_level2 != "모두 보기":
query += " AND category2 = ?" query += " AND category2 = :level2"
args.append(selected_level2) args["level2"] = selected_level2
if selected_level3 != "모두 보기": if selected_level3 != "모두 보기":
query += " AND category3 = ?" query += " AND category3 = :level3"
args.append(selected_level3) args["level3"] = selected_level3
self.cursor.execute(query, args) rows = self.db_manager.fetchall(query, args)
rows = self.cursor.fetchall()
# 트리뷰에 표시 # 트리뷰에 표시
self.category_tree.clear() self.category_tree.clear()
@ -346,14 +386,18 @@ class CMBSettingsDialog(QDialog):
top_item.setBackground(col, color) top_item.setBackground(col, color)
self.category_tree.addTopLevelItem(top_item) self.category_tree.addTopLevelItem(top_item)
except Exception as e:
QMessageBox.critical(self, "DB Loading 오류", f"레벨별 카테고리를 기준으로 트리뷰를 필터링 중 오류가 발생했습니다: {e}")
self.logger.error(f"레벨별 카테고리를 기준으로 트리뷰를 필터링 중 오류 발생: {e}", exc_info=True)
def update_cmb_settings_from_db(self): def update_cmb_settings_from_db(self):
"""DB의 crmobi_stages 테이블에서 값을 읽어와 각 단계별 설정 위젯에 반영합니다.""" """DB의 crmobi_stages 테이블에서 값을 읽어와 각 단계별 설정 위젯에 반영합니다."""
try: try:
# CrMoBi 단계 설정을 crmobi_stages 테이블에서 가져옴 # CrMoBi 단계 설정을 crmobi_stages 테이블에서 가져옴
self.cursor.execute("SELECT stage, threshold, increment_unit, extra_cost FROM crmobi_stages") query = "SELECT stage, threshold, increment_unit, extra_cost FROM crmobi_stages"
stages = self.cursor.fetchall() stages = self.db_manager.fetchall(query)
# 각 단계별 설정 값을 위젯에 적용 # 각 단계별 설정 값을 위젯에 적용
for stage in stages: for stage in stages:
@ -370,18 +414,20 @@ class CMBSettingsDialog(QDialog):
self.logger.error(f"CrMoBi 단계 설정을 위젯에 반영하는 중 오류 발생: {e}", exc_info=True) self.logger.error(f"CrMoBi 단계 설정을 위젯에 반영하는 중 오류 발생: {e}", exc_info=True)
def create_tables(self): def create_tables(self):
"""초기 DB를 생성하고 CrMoBi 단계 테이블도 추가합니다.""" """초기 DB를 생성하고 CrMoBi 단계 테이블도 추가합니다. 엑셀 파일을 읽어 DB에 데이터를 삽입합니다."""
self.db_manager.create_db_file(self.user_db_path, self.initial_db_path)
# 엑셀 파일 선택을 위한 다이얼로그
file_dialog = QFileDialog() file_dialog = QFileDialog()
excel_path, _ = file_dialog.getOpenFileName(self, "엑셀 파일 선택", "", "Excel Files (*.xlsx *.xls)") excel_path, _ = file_dialog.getOpenFileName(self, "엑셀 파일 선택", "", "Excel Files (*.xlsx *.xls)")
if excel_path: if not excel_path:
try: self.logger.error("엑셀 파일이 선택되지 않았습니다.")
db_path = os.path.join("src", "initialDB.db") return
conn = sqlite3.connect(db_path)
cursor = conn.cursor()
# 테이블 생성 try:
cursor.execute(''' # 카테고리 테이블 생성
query_create_categories = '''
CREATE TABLE IF NOT EXISTS categories ( CREATE TABLE IF NOT EXISTS categories (
id INTEGER PRIMARY KEY AUTOINCREMENT, id INTEGER PRIMARY KEY AUTOINCREMENT,
category1 TEXT, category1 TEXT,
@ -390,81 +436,86 @@ class CMBSettingsDialog(QDialog):
category4 TEXT, category4 TEXT,
crmobi_stage INTEGER DEFAULT 0 crmobi_stage INTEGER DEFAULT 0
) )
''') '''
self.db_manager.execute_query(query_create_categories)
# 엑셀 파일에서 "스스 카테고리" 시트를 읽어옴 # crmobi_stages 테이블 생성
sheet_name = '스스 카테고리' query_create_stages = '''
try:
# 시트의 첫 번째 열만 읽어옴
df = pd.read_excel(excel_path, sheet_name=sheet_name, header=None, usecols=[0])
df.columns = ["full_category"] # 열 이름 지정
# 카테고리 코드와 경로를 추출
for _, row in df.iterrows():
# 카테고리 코드 제거 및 텍스트 추출
full_text = row['full_category']
match = re.match(r'\[.*?\]\s*(.*)', full_text)
if match:
category_path = match.group(1) # 대괄호 안의 카테고리 코드를 제외한 텍스트
# 하이픈으로 카테고리 분할
category_parts = category_path.split('-')
category_levels = category_parts + [""] * (4 - len(category_parts)) # 4개로 맞추기 위해 빈 문자열 추가
# 데이터베이스에 삽입
cursor.execute('''
INSERT INTO categories (category1, category2, category3, category4)
VALUES (?, ?, ?, ?)
''', category_levels[:4])
except Exception as sheet_error:
QMessageBox.warning(self, "시트 불러오기 오류", f"'{sheet_name}' 시트에서 데이터를 읽어오는 중 오류가 발생했습니다: {sheet_error}")
# CrMoBi 단계 테이블 생성
cursor.execute('''
CREATE TABLE IF NOT EXISTS crmobi_stages ( CREATE TABLE IF NOT EXISTS crmobi_stages (
stage INTEGER PRIMARY KEY, stage INTEGER PRIMARY KEY,
threshold INTEGER, threshold INTEGER,
increment_unit INTEGER, increment_unit INTEGER,
extra_cost INTEGER extra_cost INTEGER
) )
''') '''
self.db_manager.execute_query(query_create_stages)
# 초기 데이터 설정 (원하는 경우) # 초기 crmobi_stages 데이터 삽입
cursor.execute('DELETE FROM crmobi_stages') delete_stages_query = 'DELETE FROM crmobi_stages'
conn.commit() self.db_manager.execute_query(delete_stages_query)
for i in range(1, 4): for i in range(1, 4):
cursor.execute(''' insert_stage_query = '''
INSERT INTO crmobi_stages (stage, threshold, increment_unit, extra_cost) INSERT INTO crmobi_stages (stage, threshold, increment_unit, extra_cost)
VALUES (?, ?, ?, ?) VALUES (:stage, :threshold, :increment_unit, :extra_cost)
''', (i, 100000 * i, 20000, 5000 * i)) '''
params = {"stage": i, "threshold": 100000 * i, "increment_unit": 20000, "extra_cost": 5000 * i}
self.db_manager.execute_query(insert_stage_query, params)
# 엑셀 파일에서 데이터 읽기
sheet_name = '스스 카테고리'
df = pd.read_excel(excel_path, sheet_name=sheet_name, header=None, usecols=[0])
df.columns = ["full_category"]
# 카테고리 데이터를 데이터베이스에 삽입
for _, row in df.iterrows():
full_text = row['full_category']
match = re.match(r'\[.*?\]\s*(.*)', full_text)
if match:
category_path = match.group(1)
category_parts = category_path.split('-')
category_levels = category_parts + [""] * (4 - len(category_parts)) # 4개로 맞추기 위해 빈 문자열 추가
insert_category_query = '''
INSERT INTO categories (category1, category2, category3, category4)
VALUES (:category1, :category2, :category3, :category4)
'''
category_params = {
"category1": category_levels[0],
"category2": category_levels[1],
"category3": category_levels[2],
"category4": category_levels[3]
}
self.db_manager.execute_query(insert_category_query, category_params)
conn.commit()
conn.close()
QMessageBox.information(self, "DB 생성", "DB가 성공적으로 생성되었습니다.") QMessageBox.information(self, "DB 생성", "DB가 성공적으로 생성되었습니다.")
except Exception as e: except Exception as e:
QMessageBox.critical(self, "DB 생성 오류", f"DB 생성 중 오류가 발생했습니다: {e}") QMessageBox.critical(self, "DB 생성 오류", f"DB 생성 중 오류가 발생했습니다: {e}")
self.logger.error(f"DB 생성 중 오류 발생: {e}", exc_info=True)
def load_db_to_table(self, search_text=None, cmb_stage=None): def load_db_to_table(self, search_text=None, cmb_stage=None):
"""DB에서 데이터를 읽어와 테이블에 표시하고 배경색을 설정합니다.""" """DB에서 데이터를 읽어와 테이블에 표시하고 배경색을 설정합니다."""
self.logger.debug(f"DB에서 데이터를 읽어와 테이블을 생성")
try:
self.category_tree.clear() self.category_tree.clear()
# 기본 쿼리와 조건을 설정 # 기본 쿼리와 조건을 설정
query = '''SELECT id, 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 = [] params = {}
# 검색어가 있을 경우 WHERE 조건 추가 # 검색어가 있을 경우 WHERE 조건 추가
if search_text: if search_text:
query += ''' AND (category1 LIKE ? OR category2 LIKE ? OR category3 LIKE ? OR category4 LIKE ?)''' query += ''' AND (category1 LIKE :search_text OR category2 LIKE :search_text OR category3 LIKE :search_text OR category4 LIKE :search_text)'''
args.extend([f"%{search_text}%"] * 4) params["search_text"] = f"%{search_text}%"
# CMB 단계 필터링 조건 추가 # CMB 단계 필터링 조건 추가
if cmb_stage: if cmb_stage:
query += " AND crmobi_stage = ?" query += " AND crmobi_stage = :cmb_stage"
args.append(cmb_stage) params["cmb_stage"] = cmb_stage
self.cursor.execute(query, args) # 쿼리 실행 및 데이터 가져오기
rows = self.cursor.fetchall() rows = self.db_manager.fetchall(query, params)
# id 기준 오름차순 정렬을 위해 정렬 # id 기준 오름차순 정렬을 위해 정렬
rows = sorted(rows, key=lambda x: int(x[0])) # x[0]는 id 열 rows = sorted(rows, key=lambda x: int(x[0])) # x[0]는 id 열
@ -498,6 +549,9 @@ class CMBSettingsDialog(QDialog):
# 초기 정렬 기준을 id 열로 설정하고 오름차순 정렬 # 초기 정렬 기준을 id 열로 설정하고 오름차순 정렬
self.category_tree.setSortingEnabled(True) self.category_tree.setSortingEnabled(True)
self.category_tree.sortByColumn(0, Qt.AscendingOrder) # ID 열을 기준으로 오름차순 정렬 self.category_tree.sortByColumn(0, Qt.AscendingOrder) # ID 열을 기준으로 오름차순 정렬
except Exception as e:
QMessageBox.critical(self, "DB Loading 오류", f"DB 테이블을 읽는 중 오류발생: {e}")
self.logger.error(f"load_db_to_table 실행 중 오류: {e}", exc_info=True)
def set_column_widths(self): def set_column_widths(self):
"""ID 열 너비를 일정하게 설정""" """ID 열 너비를 일정하게 설정"""
@ -507,6 +561,8 @@ class CMBSettingsDialog(QDialog):
def sort_by_column(self, index): def sort_by_column(self, index):
"""클릭된 열을 기준으로 오름차순/내림차순으로 정렬""" """클릭된 열을 기준으로 오름차순/내림차순으로 정렬"""
self.logger.debug(f"클릭된 열을 기준으로 오름차순/내림차순으로 정렬")
try:
# 정렬 역할을 UserRole로 설정하여 ID 필드가 정수로 정렬되도록 설정 # 정렬 역할을 UserRole로 설정하여 ID 필드가 정수로 정렬되도록 설정
self.category_tree.setSortingEnabled(False) # 정렬을 일시적으로 비활성화 self.category_tree.setSortingEnabled(False) # 정렬을 일시적으로 비활성화
self.category_tree.sortItems(index, self.sort_order) self.category_tree.sortItems(index, self.sort_order)
@ -514,33 +570,52 @@ class CMBSettingsDialog(QDialog):
# 정렬 순서를 토글 # 정렬 순서를 토글
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
except Exception as e:
self.logger.error(f"클릭된 열을 기준으로 오름차순/내림차순으로 정렬 중 오류 발생: {e}", exc_info=True)
def apply_crmobi_stage(self, stage): def apply_crmobi_stage(self, stage):
"""선택된 카테고리에 CrMoBi 단계를 적용하고 DB에 저장.""" """선택된 카테고리에 CrMoBi 단계를 적용하고 DB에 저장."""
self.logger.debug(f"선택된 카테고리에 CMB [{stage}]단계 적용")
try:
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(1, 5)] # ID 열 제외 category_values = {
"category1": item.text(1),
"category2": item.text(2),
"category3": item.text(3),
"category4": item.text(4),
"stage": stage
}
# DB 업데이트 # DB 업데이트
self.cursor.execute('''UPDATE categories self.db_manager.execute_query(
SET crmobi_stage = ? '''UPDATE categories
WHERE category1 = ? AND category2 = ? AND category3 = ? AND category4 = ?''', SET crmobi_stage = :stage
[stage] + category_values) WHERE category1 = :category1 AND category2 = :category2 AND category3 = :category3 AND category4 = :category4''',
category_values
self.conn.commit() # 변경사항 저장 )
self.load_db_to_table() # 트리 새로고침 self.load_db_to_table() # 트리 새로고침
except Exception as e:
QMessageBox.critical(self, "DB Update 오류", f"크무비 단계 적용 중 오류가 발생했습니다: {e}")
self.logger.error(f"CMB 단계 적용 오류 발생: {e}", exc_info=True)
def remove_cmb_stage(self): def remove_cmb_stage(self):
"""선택된 카테고리의 CMB 단계를 해제하고 DB에 반영.""" """선택된 카테고리의 CMB 단계를 해제하고 DB에 반영."""
self.logger.debug(f"CMB 단계해제")
try:
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_id = int(item.text(0)) # ID 열에서 값 가져오기 category_id = int(item.text(0)) # ID 열에서 값 가져오기
self.logger.debug(f"category_id : {category_id}")
# DB에서 CMB 단계를 해제 # DB에서 CMB 단계를 해제
self.cursor.execute("UPDATE categories SET crmobi_stage = 0 WHERE id = ?", (category_id,)) self.db_manager.execute_query("UPDATE categories SET crmobi_stage = 0 WHERE id = :category_id", {"category_id": category_id})
self.conn.commit()
self.load_db_to_table() # 트리 새로고침 self.load_db_to_table() # 트리 새로고침
except Exception as e:
QMessageBox.critical(self, "DB Update 오류", f"크무비 단계 해제 중 오류가 발생했습니다: {e}")
self.logger.error(f"CMB 단계해제 오류 발생: {e}", exc_info=True)
def toggle_cmb_settings(self, checked): def toggle_cmb_settings(self, checked):
"""CMB 단계 설정 영역 표시/숨기기, 공간을 완전히 제거 또는 복원합니다.""" """CMB 단계 설정 영역 표시/숨기기, 공간을 완전히 제거 또는 복원합니다."""
@ -559,8 +634,11 @@ class CMBSettingsDialog(QDialog):
# 레이아웃을 다시 갱신하여 공간을 완전히 반영 # 레이아웃을 다시 갱신하여 공간을 완전히 반영
self.layout().update() self.layout().update()
def toggle_select_all_filtered_items(self): def toggle_select_all_filtered_items(self):
"""버튼의 텍스트에 따라 전체 선택 또는 전체 해제를 수행합니다.""" """버튼의 텍스트에 따라 전체 선택 또는 전체 해제를 수행합니다."""
self.logger.debug(f"전체선택")
try:
if self.select_toggle_button.text() == "전체 선택": if self.select_toggle_button.text() == "전체 선택":
# 전체 체크 # 전체 체크
for i in range(self.category_tree.topLevelItemCount()): for i in range(self.category_tree.topLevelItemCount()):
@ -575,6 +653,9 @@ class CMBSettingsDialog(QDialog):
item.setCheckState(0, Qt.Unchecked) item.setCheckState(0, Qt.Unchecked)
# 버튼 텍스트를 "전체 선택"으로 변경 # 버튼 텍스트를 "전체 선택"으로 변경
self.select_toggle_button.setText("전체 선택") self.select_toggle_button.setText("전체 선택")
except Exception as e:
QMessageBox.critical(self, "카테고리 선택 오류", f"카테고리 전체 선택 중 오류가 발생했습니다: {e}")
self.logger.error(f"전체선택 중 오류: {e}", exc_info=True)
def save_cmb_stage_to_db(self): def save_cmb_stage_to_db(self):
"""사용자가 설정한 CMB 단계를 crmobi_stage 테이블에 저장합니다.""" """사용자가 설정한 CMB 단계를 crmobi_stage 테이블에 저장합니다."""
@ -587,24 +668,27 @@ class CMBSettingsDialog(QDialog):
extra_cost = cost_spin.value() extra_cost = cost_spin.value()
# 기존 데이터가 있는지 확인 # 기존 데이터가 있는지 확인
self.cursor.execute("SELECT COUNT(1) FROM crmobi_stages WHERE stage = ?", (stage,)) query_check_exists = "SELECT COUNT(1) FROM crmobi_stages WHERE stage = :stage"
exists = self.cursor.fetchone()[0] exists = self.db_manager.fetchone(query_check_exists, {"stage": stage})[0]
# 데이터를 업데이트 또는 삽입
if exists: if exists:
self.cursor.execute(''' query_update_stage = '''
UPDATE crmobi_stages UPDATE crmobi_stages
SET threshold = ?, increment_unit = ?, extra_cost = ? SET threshold = :threshold, increment_unit = :increment_unit, extra_cost = :extra_cost
WHERE stage = ? WHERE stage = :stage
''', (threshold, increment_unit, extra_cost, stage)) '''
self.db_manager.execute_query(query_update_stage, {
"threshold": threshold, "increment_unit": increment_unit, "extra_cost": extra_cost, "stage": stage
})
else: else:
self.cursor.execute(''' query_insert_stage = '''
INSERT INTO crmobi_stages (stage, threshold, increment_unit, extra_cost) INSERT INTO crmobi_stages (stage, threshold, increment_unit, extra_cost)
VALUES (?, ?, ?, ?) VALUES (:stage, :threshold, :increment_unit, :extra_cost)
''', (stage, threshold, increment_unit, extra_cost)) '''
self.db_manager.execute_query(query_insert_stage, {
"stage": stage, "threshold": threshold, "increment_unit": increment_unit, "extra_cost": extra_cost
})
# 변경사항 저장
self.conn.commit()
QMessageBox.information(self, "저장 성공", "CMB 단계 설정이 성공적으로 저장되었습니다.") QMessageBox.information(self, "저장 성공", "CMB 단계 설정이 성공적으로 저장되었습니다.")
except Exception as e: except Exception as e:
@ -613,17 +697,24 @@ class CMBSettingsDialog(QDialog):
def reset_db(self): def reset_db(self):
"""사용자 DB를 삭제하고 초기 DB를 로드합니다.""" """사용자 DB를 삭제하고 초기 DB를 로드합니다."""
try:
if os.path.exists(self.user_db_path): if os.path.exists(self.user_db_path):
os.remove(self.user_db_path) os.remove(self.user_db_path)
shutil.copyfile(self.initial_db_path, self.user_db_path) self.logger.debug(f"Deleted user DB at: {self.user_db_path}")
# 초기 DB 복사
self.db_manager.create_db_file(self.user_db_path, self.initial_db_path)
QMessageBox.information(self, "DB 초기화", "사용자 DB가 삭제되었습니다. 초기 DB를 로드합니다.") QMessageBox.information(self, "DB 초기화", "사용자 DB가 삭제되었습니다. 초기 DB를 로드합니다.")
self.load_db_to_table() self.load_db_to_table()
except Exception as e:
QMessageBox.critical(self, "DB 초기화 오류", f"DB 초기화 중 오류가 발생했습니다: {e}")
self.logger.error(f"DB 초기화 중 오류: {e}", exc_info=True)
def save_user_db(self): def save_user_db(self):
"""사용자가 설정한 내용을 저장하여 사용자 DB로 생성합니다.""" """사용자가 설정한 내용을 저장하여 사용자 DB로 생성합니다."""
# 테이블 생성 # 테이블 생성
self.cursor.execute(''' self.db_manager.execute_query('''
CREATE TABLE IF NOT EXISTS categories ( CREATE TABLE IF NOT EXISTS categories (
id INTEGER PRIMARY KEY AUTOINCREMENT, id INTEGER PRIMARY KEY AUTOINCREMENT,
category1 TEXT, category1 TEXT,
@ -634,7 +725,7 @@ class CMBSettingsDialog(QDialog):
''') ''')
# 기존 데이터를 모두 삭제 # 기존 데이터를 모두 삭제
self.cursor.execute("DELETE FROM categories") self.db_manager.execute_query("DELETE FROM categories")
# 테이블 데이터 삽입 # 테이블 데이터 삽입
for row_idx in range(self.category_table.rowCount()): for row_idx in range(self.category_table.rowCount()):
@ -642,7 +733,7 @@ class CMBSettingsDialog(QDialog):
self.category_table.item(row_idx, col_idx).text() if self.category_table.item(row_idx, col_idx) else "" self.category_table.item(row_idx, col_idx).text() if self.category_table.item(row_idx, col_idx) else ""
for col_idx in range(4) for col_idx in range(4)
] ]
self.cursor.execute("INSERT INTO categories (category1, category2, category3, category4) VALUES (?, ?, ?, ?)", row_data) self.db_manager.execute_query("INSERT INTO categories (category1, category2, category3, category4) VALUES (?, ?, ?, ?)", row_data)
QMessageBox.information(self, "DB 저장", "사용자 DB가 저장되었습니다.") QMessageBox.information(self, "DB 저장", "사용자 DB가 저장되었습니다.")
@ -665,6 +756,9 @@ class CMBSettingsDialog(QDialog):
def search_category(self): def search_category(self):
"""카테고리 검색 기능.""" """카테고리 검색 기능."""
self.logger.debug(f"카테고리 검색 기능")
try:
search_text = self.search_input.text().strip() search_text = self.search_input.text().strip()
if search_text: if search_text:
# 검색어가 있는 경우 필터링하여 로드 # 검색어가 있는 경우 필터링하여 로드
@ -674,18 +768,17 @@ class CMBSettingsDialog(QDialog):
WHERE category1 LIKE ? OR category2 LIKE ? OR category3 LIKE ? OR category4 LIKE ? WHERE category1 LIKE ? OR category2 LIKE ? OR category3 LIKE ? OR category4 LIKE ?
''' '''
args = [f"%{search_text}%"] * 4 args = [f"%{search_text}%"] * 4
self.cursor.execute(query, args)
else: else:
# 검색어가 없는 경우 전체 로드 # 검색어가 없는 경우 전체 로드
query = ''' query = '''
SELECT id, category1, category2, category3, category4, crmobi_stage SELECT id, category1, category2, category3, category4, crmobi_stage
FROM categories FROM categories
''' '''
self.cursor.execute(query) args = []
# 검색 결과를 트리에 표시 # 검색 결과를 트리에 표시
self.category_tree.clear() self.category_tree.clear()
rows = self.cursor.fetchall() rows = self.db_manager.fetchall(query, args)
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]])
@ -714,7 +807,12 @@ class CMBSettingsDialog(QDialog):
self.category_tree.addTopLevelItem(top_item) self.category_tree.addTopLevelItem(top_item)
def get_crmobi_stage(self, category): except Exception as e:
QMessageBox.critical(self, "카테고리 검색 오류", f"카테고리 검색 중 오류가 발생했습니다: {e}")
self.logger.error(f"카테고리 검색 중 오류: {e}", exc_info=True)
def get_crmobi_stage_by_keyword(self, category):
""" """
주어진 카테고리에 해당하는 CMB 단계가 설정되어 있는지 확인하고, 주어진 카테고리에 해당하는 CMB 단계가 설정되어 있는지 확인하고,
설정된 경우 해당 단계의 범위를 반환합니다. 설정된 경우 해당 단계의 범위를 반환합니다.
@ -735,8 +833,8 @@ class CMBSettingsDialog(QDialog):
self.logger.debug(f"category : {category}") self.logger.debug(f"category : {category}")
args = [category] * 4 args = [category] * 4
self.cursor.execute(query, args) # self.db_manager.execute(query, args)
result = self.cursor.fetchone() result = self.db_manager.fetchone(query, args)
if result and result[0] > 0: # CMB 단계가 설정되어 있을 때 if result and result[0] > 0: # CMB 단계가 설정되어 있을 때
crmobi_stage = result[0] crmobi_stage = result[0]
@ -747,8 +845,8 @@ class CMBSettingsDialog(QDialog):
FROM crmobi_stages FROM crmobi_stages
WHERE stage = ? WHERE stage = ?
''' '''
self.cursor.execute(stage_query, (crmobi_stage,)) # self.db_manager.execute(stage_query, (crmobi_stage,))
stage_result = self.cursor.fetchone() stage_result = self.db_manager.fetchone(stage_query, (crmobi_stage,))
self.logger.debug(f"stage_result : {stage_result}") self.logger.debug(f"stage_result : {stage_result}")
if stage_result: if stage_result:
@ -762,9 +860,86 @@ class CMBSettingsDialog(QDialog):
QMessageBox.critical(self, "DB 오류", f"DB 조회 중 오류가 발생했습니다: {e}") QMessageBox.critical(self, "DB 오류", f"DB 조회 중 오류가 발생했습니다: {e}")
return None return None
def get_crmobi_stage(self, category):
"""
주어진 카테고리에 해당하는 CMB 단계가 설정되어 있는지 확인하고,
설정된 경우 해당 단계의 범위를 반환합니다.
Parameters:
category (str): 카테고리 이름 (: '생활/건강-공구-에어공구-유압공구')
Returns:
tuple: (min_amount, unit_amount, extra_cost) 값이 있는 경우
None: 설정된 CMB 단계가 없는 경우
"""
try:
# 카테고리를 하이픈("-")을 기준으로 나누기
category_levels = category.split('-')
category1 = category_levels[0] if len(category_levels) > 0 else None
category2 = category_levels[1] if len(category_levels) > 1 else None
category3 = category_levels[2] if len(category_levels) > 2 else None
category4 = category_levels[3] if len(category_levels) > 3 else None
# 디버깅 로그: 카테고리 레벨별 출력
self.logger.debug(f"Parsed category levels - Level 1: {category1}, Level 2: {category2}, Level 3: {category3}, Level 4: {category4}")
# DB에서 카테고리에 대한 CMB 단계 정보를 가져옴
query = '''
SELECT crmobi_stage FROM categories
WHERE (category1 = :category1 OR :category1 IS NULL)
AND (category2 = :category2 OR :category2 IS NULL)
AND (category3 = :category3 OR :category3 IS NULL)
AND (category4 = :category4 OR :category4 IS NULL)
'''
self.logger.debug(f"Executing query to find CMB stage for category: {category}")
params = {
"category1": category1,
"category2": category2,
"category3": category3,
"category4": category4
}
self.logger.debug(f"Query arguments: {params}")
# 쿼리 실행
result = self.db_manager.fetchone(query, params)
# 디버깅 로그: 쿼리 결과 출력
self.logger.debug(f"Query result for CMB stage: {result}")
if result and result[0] > 0: # CMB 단계가 설정되어 있을 때
crmobi_stage = result[0]
self.logger.debug(f"CMB stage found: {crmobi_stage}")
# 설정된 CMB 단계에 해당하는 범위 가져오기
stage_query = '''
SELECT threshold, increment_unit, extra_cost
FROM crmobi_stages
WHERE stage = :stage
'''
self.logger.debug(f"Executing query to get stage details for stage: {crmobi_stage}")
stage_result = self.db_manager.fetchone(stage_query, {"stage": crmobi_stage})
self.logger.debug(f"Stage details result: {stage_result}")
if stage_result:
min_amount, unit_amount, extra_cost = stage_result
self.logger.debug(f"Returning stage details - Threshold: {min_amount}, Increment Unit: {unit_amount}, Extra Cost: {extra_cost}")
return (min_amount, unit_amount, extra_cost)
# CMB 단계가 설정되지 않은 경우
self.logger.debug("No CMB stage found for the given category.")
return None
except Exception as e:
self.logger.error(f"DB 조회 중 오류가 발생했습니다: {e}", exc_info=True)
QMessageBox.critical(self, "DB 오류", f"DB 조회 중 오류가 발생했습니다: {e}")
return None
def closeEvent(self, event): def closeEvent(self, event):
"""QDialog 종료 시 DB 연결 해제""" """QDialog 종료 시 DB 연결 해제"""
self.conn.close() # self.conn.close()
event.accept() event.accept()
def focusInEvent(self, event): def focusInEvent(self, event):

Binary file not shown.