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

26
Todo.List Normal file
View File

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

Binary file not shown.

Binary file not shown.

Binary file not shown.

File diff suppressed because it is too large Load Diff

View File

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

View File

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

24
gui.py
View File

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

View File

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

128
option.py
View File

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

117
option_translator.py Normal file
View File

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

View File

@ -574,8 +574,8 @@ class PriceHandler:
# 옵션 개수만큼 순회하면서 값을 수집
for i in range(1, total_options + 1):
product_cost_locator = self.product_cost_locator_template.format(i=i+1)
standard_selling_price_locator = self.standard_selling_price_locator_template.format(i=i+1)
product_cost_locator = self.product_cost_locator_template.format(index=i+1)
standard_selling_price_locator = self.standard_selling_price_locator_template.format(index=i+1)
# 각 선택자를 사용하여 요소 찾기
product_cost_element = await self.page.wait_for_selector(product_cost_locator)
@ -749,35 +749,19 @@ class PriceHandler:
"""
total_extra_shipping = 0
self.logger.debug(f"category : {category}")
cmb_config = self.cmb_diag.get_crmobi_stage(category)
# locator_manager에서 카테고리별 해외배송비 설정을 불러옴
category_data = self.locator_manager.get_category_data(category)
# cmb_diag에서 카테고리별 해외배송비 설정을 불러옴
category_data = self.cmb_diag.get_crmobi_stage(category)
if category_data: # 카테고리 설정이 있는지 확인
threshold_index = 1
threshold, unit, extra_shipping = category_data
# threshold와 extra_shipping의 dynamic한 처리
while True:
threshold_key = f'threshold_{threshold_index}'
extra_shipping_key = f'extra_shipping_{threshold_index}'
unit_key = f'unit_{threshold_index}'
if product_price > threshold:
excess_amount = product_price - threshold
increments = excess_amount // unit
total_extra_shipping += increments * extra_shipping
self.logger.debug(f"{category} 카테고리, threshold {threshold}을 초과하여 추가 해외배송비 {increments * extra_shipping} 추가 적용")
# threshold_key가 존재하지 않으면 종료
if threshold_key not in category_data or extra_shipping_key not in category_data or unit_key not in category_data:
break
threshold = int(category_data.get(threshold_key, 0))
extra_shipping = int(category_data.get(extra_shipping_key, 0))
unit = int(category_data.get(unit_key, 0))
if product_price > threshold:
excess_amount = product_price - threshold
increments = excess_amount // unit
total_extra_shipping += increments * extra_shipping
self.logger.debug(f"{category} 카테고리, threshold {threshold}을 초과하여 추가 해외배송비 {increments * extra_shipping} 추가 적용")
threshold_index += 1 # 다음 구간으로 이동
else:
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)
from PySide6.QtCore import Qt
from PySide6.QtGui import QFont, QColor
import sqlite3
from src.DatabaseManager import DatabaseManager
# import sqlite3
import shutil
import os, re
import pandas as pd
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)
self.setWindowTitle("CMB 설정")
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.debug = debug
# DB 파일 경로 설정
self.user_db_path = os.path.join("src", "userDB.db")
self.initial_db_path = os.path.join("src", "initialDB.db")
# self.user_db_path = os.path.join("src", "userDB.db")
# self.initial_db_path = os.path.join("src", "initialDB.db")
# userDB 체크 및 초기화
if not os.path.exists(self.user_db_path):
shutil.copyfile(self.initial_db_path, self.user_db_path)
# # userDB 체크 및 초기화
# if not os.path.exists(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()
@ -46,18 +55,19 @@ class CMBSettingsDialog(QDialog):
self.search_btn.setDefault(False)
self.search_btn.clicked.connect(self.search_category)
search_layout.addWidget(self.search_input,0,1,1,3)
search_layout.addWidget(self.search_btn,0,2,1,1)
self.cmb_view_btn = QPushButton("모두 보기")
search_layout.addWidget(self.search_input,0,1,1,8)
search_layout.addWidget(self.search_btn,0,9,1,1)
self.cmb_view_btn = QPushButton("CMB-ALL")
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레벨 콤보박스와 라벨 설정
self.category_combo_label = QLabel("카테고리")
self.level1_combo = QComboBox()
self.level2_combo = QComboBox()
self.level3_combo = QComboBox()
self.reset_combo_btn = QPushButton("콤보리셋")
self.reset_combo_btn = QPushButton("Reset")
# 기본적으로 "모두 보기" 옵션 추가
self.level1_combo.addItem("모두 보기")
@ -66,13 +76,13 @@ class CMBSettingsDialog(QDialog):
# 레벨 필터링 라벨 추가
# 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(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(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)
search_layout.addWidget(self.level3_combo,1,7,1,3)
search_layout.addWidget(self.reset_combo_btn,1,10,1,1)
# 콤보박스의 신호 연결
self.level1_combo.currentTextChanged.connect(self.update_level2_combo)
@ -243,117 +253,151 @@ class CMBSettingsDialog(QDialog):
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])
self.logger.debug("1레벨 카테고리를 업데이트")
try:
query = "SELECT DISTINCT category1 FROM categories WHERE category1 IS NOT NULL"
rows = self.db_manager.fetchall(query)
for row in rows:
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):
"""1레벨 선택 시, 2레벨 콤보박스를 업데이트"""
selected_level1 = self.level1_combo.currentText()
self.level2_combo.clear()
self.level2_combo.addItem("모두 보기")
self.logger.debug("2레벨 카테고리를 업데이트")
try:
selected_level1 = self.level1_combo.currentText()
self.level2_combo.clear()
self.level2_combo.addItem("모두 보기")
if selected_level1 == "모두 보기":
return
# "모두 보기"가 선택된 경우에는 필터링하지 않음
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()
# selected_level1이 유효한 값일 경우에만 쿼리를 실행
if selected_level1:
query = "SELECT DISTINCT category2 FROM categories WHERE category1 = :level1 AND category2 IS NOT NULL"
rows = self.db_manager.fetchall(query, {"level1": selected_level1})
for row in rows:
self.level2_combo.addItem(row[0])
if rows is None:
self.logger.error("DB에서 데이터를 가져오지 못했습니다. 쿼리를 확인해주세요.")
return
for row in rows:
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):
"""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
self.logger.debug("3레벨 카테고리를 업데이트")
try:
selected_level1 = self.level1_combo.currentText()
selected_level2 = self.level2_combo.currentText()
self.level3_combo.clear()
self.level3_combo.addItem("모두 보기")
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()
# 파라미터를 제대로 전달하여 쿼리를 수행
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})
for row in rows:
self.level3_combo.addItem(row[0])
if rows is None:
self.logger.error("DB에서 데이터를 가져오지 못했습니다. 쿼리를 확인해주세요.")
return
for row in rows:
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):
"""1레벨, 2레벨, 3레벨 콤보박스를 초기화하고 QTreeWidget을 전체 항목으로 필터링합니다."""
# 각 레벨 콤보박스 초기화
self.level1_combo.setCurrentIndex(0)
self.level2_combo.clear()
self.level3_combo.clear()
self.logger.debug("1레벨, 2레벨, 3레벨 카테고리를 초기화")
try:
"""1레벨, 2레벨, 3레벨 콤보박스를 초기화하고 QTreeWidget을 전체 항목으로 필터링합니다."""
# 각 레벨 콤보박스 초기화
self.level1_combo.setCurrentIndex(0)
self.level2_combo.clear()
self.level3_combo.clear()
# QTreeWidget 필터링 초기화 (모든 항목 표시)
self.load_db_to_table() # 이 메서드는 전체 데이터를 다시 로드하는 메서드입니다.
# QTreeWidget 필터링 초기화 (모든 항목 표시)
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):
"""선택된 1레벨, 2레벨, 3레벨 카테고리를 기준으로 트리뷰를 필터링"""
selected_level1 = self.level1_combo.currentText()
selected_level2 = self.level2_combo.currentText()
selected_level3 = self.level3_combo.currentText()
self.logger.debug(f"레벨별 카테고리를 기준으로 트리뷰를 필터링")
try:
"""선택된 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 = []
# 기본 쿼리와 조건을 설정
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)
if selected_level1 != "모두 보기":
query += " AND category1 = :level1"
args["level1"] = selected_level1
if selected_level2 != "모두 보기":
query += " AND category2 = :level2"
args["level2"] = selected_level2
if selected_level3 != "모두 보기":
query += " AND category3 = :level3"
args["level3"] = selected_level3
self.cursor.execute(query, args)
rows = self.cursor.fetchall()
rows = self.db_manager.fetchall(query, args)
# 트리뷰에 표시
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)
# 트리뷰에 표시
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
# 단계별 배경색 설정 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)
# 행 전체에 배경색 적용
if color:
for col in range(6): # 열의 개수에 따라 반복
top_item.setBackground(col, color)
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)
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()
query = "SELECT stage, threshold, increment_unit, extra_cost FROM crmobi_stages"
stages = self.db_manager.fetchall(query)
# 각 단계별 설정 값을 위젯에 적용
for stage in stages:
@ -370,134 +414,144 @@ class CMBSettingsDialog(QDialog):
self.logger.error(f"CrMoBi 단계 설정을 위젯에 반영하는 중 오류 발생: {e}", exc_info=True)
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()
excel_path, _ = file_dialog.getOpenFileName(self, "엑셀 파일 선택", "", "Excel Files (*.xlsx *.xls)")
if excel_path:
try:
db_path = os.path.join("src", "initialDB.db")
conn = sqlite3.connect(db_path)
cursor = conn.cursor()
if not excel_path:
self.logger.error("엑셀 파일이 선택되지 않았습니다.")
return
# 테이블 생성
cursor.execute('''
CREATE TABLE IF NOT EXISTS categories (
id INTEGER PRIMARY KEY AUTOINCREMENT,
category1 TEXT,
category2 TEXT,
category3 TEXT,
category4 TEXT,
crmobi_stage INTEGER DEFAULT 0
)
''')
try:
# 카테고리 테이블 생성
query_create_categories = '''
CREATE TABLE IF NOT EXISTS categories (
id INTEGER PRIMARY KEY AUTOINCREMENT,
category1 TEXT,
category2 TEXT,
category3 TEXT,
category4 TEXT,
crmobi_stage INTEGER DEFAULT 0
)
'''
self.db_manager.execute_query(query_create_categories)
# 엑셀 파일에서 "스스 카테고리" 시트를 읽어옴
sheet_name = '스스 카테고리'
try:
# 시트의 첫 번째 열만 읽어옴
df = pd.read_excel(excel_path, sheet_name=sheet_name, header=None, usecols=[0])
df.columns = ["full_category"] # 열 이름 지정
# crmobi_stages 테이블 생성
query_create_stages = '''
CREATE TABLE IF NOT EXISTS crmobi_stages (
stage INTEGER PRIMARY KEY,
threshold INTEGER,
increment_unit INTEGER,
extra_cost INTEGER
)
'''
self.db_manager.execute_query(query_create_stages)
# 카테고리 코드와 경로를 추출
for _, row in df.iterrows():
# 카테고리 코드 제거 및 텍스트 추출
full_text = row['full_category']
match = re.match(r'\[.*?\]\s*(.*)', full_text)
if match:
category_path = match.group(1) # 대괄호 안의 카테고리 코드를 제외한 텍스트
# 초기 crmobi_stages 데이터 삽입
delete_stages_query = 'DELETE FROM crmobi_stages'
self.db_manager.execute_query(delete_stages_query)
# 하이픈으로 카테고리 분할
category_parts = category_path.split('-')
category_levels = category_parts + [""] * (4 - len(category_parts)) # 4개로 맞추기 위해 빈 문자열 추가
for i in range(1, 4):
insert_stage_query = '''
INSERT INTO crmobi_stages (stage, threshold, increment_unit, extra_cost)
VALUES (:stage, :threshold, :increment_unit, :extra_cost)
'''
params = {"stage": i, "threshold": 100000 * i, "increment_unit": 20000, "extra_cost": 5000 * i}
self.db_manager.execute_query(insert_stage_query, params)
# 데이터베이스에 삽입
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}")
# 엑셀 파일에서 데이터 읽기
sheet_name = '스스 카테고리'
df = pd.read_excel(excel_path, sheet_name=sheet_name, header=None, usecols=[0])
df.columns = ["full_category"]
# CrMoBi 단계 테이블 생성
cursor.execute('''
CREATE TABLE IF NOT EXISTS crmobi_stages (
stage INTEGER PRIMARY KEY,
threshold INTEGER,
increment_unit INTEGER,
extra_cost INTEGER
)
''')
# 카테고리 데이터를 데이터베이스에 삽입
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('DELETE FROM crmobi_stages')
conn.commit()
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)
for i in range(1, 4):
cursor.execute('''
INSERT INTO crmobi_stages (stage, threshold, increment_unit, extra_cost)
VALUES (?, ?, ?, ?)
''', (i, 100000 * i, 20000, 5000 * i))
QMessageBox.information(self, "DB 생성", "DB가 성공적으로 생성되었습니다.")
conn.commit()
conn.close()
QMessageBox.information(self, "DB 생성", "DB가 성공적으로 생성되었습니다.")
except Exception as e:
QMessageBox.critical(self, "DB 생성 오류", f"DB 생성 중 오류가 발생했습니다: {e}")
except Exception as 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):
"""DB에서 데이터를 읽어와 테이블에 표시하고 배경색을 설정합니다."""
self.category_tree.clear()
self.logger.debug(f"DB에서 데이터를 읽어와 테이블을 생성")
try:
self.category_tree.clear()
# 기본 쿼리와 조건을 설정
query = '''SELECT id, category1, category2, category3, category4, crmobi_stage FROM categories WHERE 1=1'''
args = []
# 기본 쿼리와 조건을 설정
query = '''SELECT id, category1, category2, category3, category4, crmobi_stage FROM categories WHERE 1=1'''
params = {}
# 검색어가 있을 경우 WHERE 조건 추가
if search_text:
query += ''' AND (category1 LIKE ? OR category2 LIKE ? OR category3 LIKE ? OR category4 LIKE ?)'''
args.extend([f"%{search_text}%"] * 4)
# 검색어가 있을 경우 WHERE 조건 추가
if search_text:
query += ''' AND (category1 LIKE :search_text OR category2 LIKE :search_text OR category3 LIKE :search_text OR category4 LIKE :search_text)'''
params["search_text"] = f"%{search_text}%"
# CMB 단계 필터링 조건 추가
if cmb_stage:
query += " AND crmobi_stage = ?"
args.append(cmb_stage)
# CMB 단계 필터링 조건 추가
if cmb_stage:
query += " AND crmobi_stage = :cmb_stage"
params["cmb_stage"] = cmb_stage
self.cursor.execute(query, args)
rows = self.cursor.fetchall()
# 쿼리 실행 및 데이터 가져오기
rows = self.db_manager.fetchall(query, params)
# id 기준 오름차순 정렬을 위해 정렬
rows = sorted(rows, key=lambda x: int(x[0])) # x[0]는 id 열
# id 기준 오름차순 정렬을 위해 정렬
rows = sorted(rows, key=lambda x: int(x[0])) # x[0]는 id 열
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)
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))
# id 값을 정수로 설정하여 정렬 시 정수 기준으로 처리되도록 함
top_item.setData(0, Qt.UserRole, int(row_id))
# 단계별 배경색 설정 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
# 단계별 배경색 설정 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)
# 행 전체에 배경색 적용
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 열을 기준으로 오름차순 정렬
# 초기 정렬 기준을 id 열로 설정하고 오름차순 정렬
self.category_tree.setSortingEnabled(True)
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):
"""ID 열 너비를 일정하게 설정"""
@ -507,40 +561,61 @@ class CMBSettingsDialog(QDialog):
def sort_by_column(self, index):
"""클릭된 열을 기준으로 오름차순/내림차순으로 정렬"""
# 정렬 역할을 UserRole로 설정하여 ID 필드가 정수로 정렬되도록 설정
self.category_tree.setSortingEnabled(False) # 정렬을 일시적으로 비활성화
self.category_tree.sortItems(index, self.sort_order)
self.category_tree.setSortingEnabled(True) # 정렬을 다시 활성화
self.logger.debug(f"클릭된 열을 기준으로 오름차순/내림차순으로 정렬")
try:
# 정렬 역할을 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
except Exception as e:
self.logger.error(f"클릭된 열을 기준으로 오름차순/내림차순으로 정렬 중 오류 발생: {e}", exc_info=True)
def apply_crmobi_stage(self, stage):
"""선택된 카테고리에 CrMoBi 단계를 적용하고 DB에 저장."""
for i in range(self.category_tree.topLevelItemCount()):
item = self.category_tree.topLevelItem(i)
if item.checkState(0) == Qt.Checked:
category_values = [item.text(j) for j in range(1, 5)] # ID 열 제외
# DB 업데이트
self.cursor.execute('''UPDATE categories
SET crmobi_stage = ?
WHERE category1 = ? AND category2 = ? AND category3 = ? AND category4 = ?''',
[stage] + category_values)
self.conn.commit() # 변경사항 저장
self.load_db_to_table() # 트리 새로고침
self.logger.debug(f"선택된 카테고리에 CMB [{stage}]단계 적용")
try:
for i in range(self.category_tree.topLevelItemCount()):
item = self.category_tree.topLevelItem(i)
if item.checkState(0) == Qt.Checked:
category_values = {
"category1": item.text(1),
"category2": item.text(2),
"category3": item.text(3),
"category4": item.text(4),
"stage": stage
}
# DB 업데이트
self.db_manager.execute_query(
'''UPDATE categories
SET crmobi_stage = :stage
WHERE category1 = :category1 AND category2 = :category2 AND category3 = :category3 AND category4 = :category4''',
category_values
)
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):
"""선택된 카테고리의 CMB 단계를 해제하고 DB에 반영."""
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.logger.debug(f"CMB 단계해제")
try:
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 열에서 값 가져오기
self.logger.debug(f"category_id : {category_id}")
# DB에서 CMB 단계를 해제
self.db_manager.execute_query("UPDATE categories SET crmobi_stage = 0 WHERE id = :category_id", {"category_id": category_id})
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)
self.conn.commit()
self.load_db_to_table() # 트리 새로고침
def toggle_cmb_settings(self, checked):
"""CMB 단계 설정 영역 표시/숨기기, 공간을 완전히 제거 또는 복원합니다."""
@ -559,22 +634,28 @@ class CMBSettingsDialog(QDialog):
# 레이아웃을 다시 갱신하여 공간을 완전히 반영
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("전체 선택")
self.logger.debug(f"전체선택")
try:
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("전체 선택")
except Exception as e:
QMessageBox.critical(self, "카테고리 선택 오류", f"카테고리 전체 선택 중 오류가 발생했습니다: {e}")
self.logger.error(f"전체선택 중 오류: {e}", exc_info=True)
def save_cmb_stage_to_db(self):
"""사용자가 설정한 CMB 단계를 crmobi_stage 테이블에 저장합니다."""
@ -587,24 +668,27 @@ class CMBSettingsDialog(QDialog):
extra_cost = cost_spin.value()
# 기존 데이터가 있는지 확인
self.cursor.execute("SELECT COUNT(1) FROM crmobi_stages WHERE stage = ?", (stage,))
exists = self.cursor.fetchone()[0]
query_check_exists = "SELECT COUNT(1) FROM crmobi_stages WHERE stage = :stage"
exists = self.db_manager.fetchone(query_check_exists, {"stage": stage})[0]
# 데이터를 업데이트 또는 삽입
if exists:
self.cursor.execute('''
query_update_stage = '''
UPDATE crmobi_stages
SET threshold = ?, increment_unit = ?, extra_cost = ?
WHERE stage = ?
''', (threshold, increment_unit, extra_cost, stage))
SET threshold = :threshold, increment_unit = :increment_unit, extra_cost = :extra_cost
WHERE stage = :stage
'''
self.db_manager.execute_query(query_update_stage, {
"threshold": threshold, "increment_unit": increment_unit, "extra_cost": extra_cost, "stage": stage
})
else:
self.cursor.execute('''
query_insert_stage = '''
INSERT INTO crmobi_stages (stage, threshold, increment_unit, extra_cost)
VALUES (?, ?, ?, ?)
''', (stage, threshold, increment_unit, extra_cost))
VALUES (: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 단계 설정이 성공적으로 저장되었습니다.")
except Exception as e:
@ -613,17 +697,24 @@ class CMBSettingsDialog(QDialog):
def reset_db(self):
"""사용자 DB를 삭제하고 초기 DB를 로드합니다."""
if os.path.exists(self.user_db_path):
os.remove(self.user_db_path)
shutil.copyfile(self.initial_db_path, self.user_db_path)
try:
if os.path.exists(self.user_db_path):
os.remove(self.user_db_path)
self.logger.debug(f"Deleted user DB at: {self.user_db_path}")
QMessageBox.information(self, "DB 초기화", "사용자 DB가 삭제되었습니다. 초기 DB를 로드합니다.")
self.load_db_to_table()
# 초기 DB 복사
self.db_manager.create_db_file(self.user_db_path, self.initial_db_path)
QMessageBox.information(self, "DB 초기화", "사용자 DB가 삭제되었습니다. 초기 DB를 로드합니다.")
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):
"""사용자가 설정한 내용을 저장하여 사용자 DB로 생성합니다."""
# 테이블 생성
self.cursor.execute('''
self.db_manager.execute_query('''
CREATE TABLE IF NOT EXISTS categories (
id INTEGER PRIMARY KEY AUTOINCREMENT,
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()):
@ -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 ""
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가 저장되었습니다.")
@ -665,56 +756,63 @@ class CMBSettingsDialog(QDialog):
def search_category(self):
"""카테고리 검색 기능."""
search_text = self.search_input.text().strip()
if search_text:
# 검색어가 있는 경우 필터링하여 로드
query = '''
SELECT id, category1, category2, category3, category4, crmobi_stage
FROM categories
WHERE category1 LIKE ? OR category2 LIKE ? OR category3 LIKE ? OR category4 LIKE ?
'''
args = [f"%{search_text}%"] * 4
self.cursor.execute(query, args)
else:
# 검색어가 없는 경우 전체 로드
query = '''
SELECT id, category1, category2, category3, category4, crmobi_stage
FROM categories
'''
self.cursor.execute(query)
self.logger.debug(f"카테고리 검색 기능")
# 검색 결과를 트리에 표시
self.category_tree.clear()
rows = self.cursor.fetchall()
for row_data in rows:
# top_item = QTreeWidgetItem([str(data) if data else "" for data in row_data[:4]])
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 "미적용"])
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:
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) # 라이트핑크
try:
search_text = self.search_input.text().strip()
if search_text:
# 검색어가 있는 경우 필터링하여 로드
query = '''
SELECT id, category1, category2, category3, category4, crmobi_stage
FROM categories
WHERE category1 LIKE ? OR category2 LIKE ? OR category3 LIKE ? OR category4 LIKE ?
'''
args = [f"%{search_text}%"] * 4
else:
color = None
# 검색어가 없는 경우 전체 로드
query = '''
SELECT id, category1, category2, category3, category4, crmobi_stage
FROM categories
'''
args = []
# 행 전체에 배경색 적용
if color:
for col in range(6): # 열의 개수에 따라 반복
top_item.setBackground(col, color)
# 검색 결과를 트리에 표시
self.category_tree.clear()
rows = self.db_manager.fetchall(query, args)
self.category_tree.addTopLevelItem(top_item)
for row_data in rows:
# top_item = QTreeWidgetItem([str(data) if data else "" for data in row_data[:4]])
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 "미적용"])
def get_crmobi_stage(self, category):
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:
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)
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 단계가 설정되어 있는지 확인하고,
설정된 경우 해당 단계의 범위를 반환합니다.
@ -735,8 +833,8 @@ class CMBSettingsDialog(QDialog):
self.logger.debug(f"category : {category}")
args = [category] * 4
self.cursor.execute(query, args)
result = self.cursor.fetchone()
# self.db_manager.execute(query, args)
result = self.db_manager.fetchone(query, args)
if result and result[0] > 0: # CMB 단계가 설정되어 있을 때
crmobi_stage = result[0]
@ -747,8 +845,8 @@ class CMBSettingsDialog(QDialog):
FROM crmobi_stages
WHERE stage = ?
'''
self.cursor.execute(stage_query, (crmobi_stage,))
stage_result = self.cursor.fetchone()
# self.db_manager.execute(stage_query, (crmobi_stage,))
stage_result = self.db_manager.fetchone(stage_query, (crmobi_stage,))
self.logger.debug(f"stage_result : {stage_result}")
if stage_result:
@ -762,9 +860,86 @@ class CMBSettingsDialog(QDialog):
QMessageBox.critical(self, "DB 오류", f"DB 조회 중 오류가 발생했습니다: {e}")
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):
"""QDialog 종료 시 DB 연결 해제"""
self.conn.close()
# self.conn.close()
event.accept()
def focusInEvent(self, event):

Binary file not shown.