This commit is contained in:
Envy_PC 2025-02-17 15:29:21 +09:00
parent 3ab88bef1e
commit 89fdd21058
7 changed files with 505 additions and 94 deletions

View File

@ -636,6 +636,8 @@ class Scrapper1(QThread):
실제 스크래핑 로직은 필요에 따라 구현하세요.
"""
try:
self.logger.log(f"프로그레스바 초기화")
self.progress_signal.emit(0)
self.logger.log("검색 페이지 접속 중...", level=20)
import urllib.parse
encoded_query = urllib.parse.quote(self.search_query)

View File

@ -27,73 +27,147 @@ class ExcelExporter(QObject):
self.logger.log(f"DB에서 데이터 로드 중 오류 발생: {e}", level=logging.ERROR, exc_info=True)
return pd.DataFrame()
def save_to_excel(self, output_path="output.xlsx"):
# def save_to_excel(self, output_path="output.xlsx"):
# df = self.fetch_data_from_db()
# if df.empty:
# self.logger.log(f"DB에서 불러온 데이터가 없습니다.", level=logging.WARNING)
# return False # 성공 여부 반환
# # 조건에 맞는 데이터 필터링
# filtered_df = df[(df['is_valid'] == 1) & (df['is_export'] == 0)]
# if filtered_df.empty:
# self.logger.log(f"조건에 맞는 데이터가 없습니다.", level=logging.WARNING)
# return False # 성공 여부 반환
# app = xw.App(visible=False)
# self.logger.log(f"xlwings 시작", level=logging.DEBUG)
# try:
# total_rows = len(df)
# for i in range(0, total_rows, 50):
# df_subset = df.iloc[i:i+50]
# self.logger.log(f"{i}번째 출력할 데이터:\n{df_subset}", level=logging.DEBUG) # 데이터 검증 로그 추가
# part_file_name = output_path.replace('.xlsx', f'_part{i//50 + 1}.xlsx')
# shutil.copy(self.base_excel_path, part_file_name)
# self.logger.log(f"기본 엑셀 파일 '{self.base_excel_path}' 복사 완료", level=logging.DEBUG)
# wb = xw.Book(part_file_name)
# ws = wb.sheets['multi_ss']
# for index, row in df_subset.iterrows():
# row_num = 4 + (index % 50)
# self.logger.log(f"{index + 1}번째 행 기록 시작: B{row_num}, C{row_num}, D{row_num}, F{row_num}, G{row_num}, H{row_num}", level=logging.DEBUG) # 셀 위치 로그 추가
# ws.range(f'B{row_num}').value = row['pc_url']
# ws.range(f'C{row_num}').value = row['generated_Title']
# ws.range(f'D{row_num}').value = row['margin_price']
# ws.range(f'F{row_num}').value = row['tags']
# ws.range(f'G{row_num}').value = row['category_code']
# ws.range(f'H{row_num}').value = row['memo']
# # 데이터베이스 업데이트
# self.db_manager.update_item({
# 'id': row['id'],
# 'generated_Title': row.get('generated_Title', None),
# 'category_code': row['category_code'],
# 'tags': row['tags'],
# 'margin_price': row.get('margin_price', None),
# 'memo': row['memo'],
# 'is_valid': row['is_valid'],
# 'is_export': 1 # is_export를 1로 설정
# })
# self.logger.log(f"{index + 1}번째 행 기록 완료", level=logging.DEBUG)
# # 프로그레스바 업데이트
# progress = int((index + 1) / total_rows * 100)
# self.progress_signal.emit(progress)
# wb.save(part_file_name) # SaveCopyAs 대신 save 사용
# wb.close()
# self.saved_files.append(part_file_name)
# self.logger.log(f"파일 '{part_file_name}'에 데이터가 저장되었습니다.", level=logging.INFO)
# return True # 성공 여부 반환
# except Exception as e:
# self.logger.log(f"엑셀 저장 중 예외 발생: {e}", level=logging.ERROR, exc_info=True)
# return False # 실패 시 False 반환
# finally:
# app.quit()
# self.logger.log(f"xlwings 종료", level=logging.DEBUG)
def save_to_excel(self, output_path="output.xlsx", options=None):
"""
options: dict with keys:
- shoppinglens (bool)
- title_generation (bool)
- margin_price (bool)
- category_input (bool)
"""
if options is None:
options = {"shoppinglens": False, "title_generation": False, "margin_price": False, "category_input": False}
df = self.fetch_data_from_db()
if df.empty:
self.logger.log(f"DB에서 불러온 데이터가 없습니다.", level=logging.WARNING)
return False # 성공 여부 반환
self.logger.log("DB에서 불러온 데이터가 없습니다.", level=logging.WARNING)
return False
# 조건에 맞는 데이터 필터링
# 기본 필터: 유효한 데이터(is_valid=1, is_export=0)
filtered_df = df[(df['is_valid'] == 1) & (df['is_export'] == 0)]
if filtered_df.empty:
self.logger.log(f"조건에 맞는 데이터가 없습니다.", level=logging.WARNING)
return False # 성공 여부 반환
self.logger.log("조건에 맞는 데이터가 없습니다.", level=logging.WARNING)
return False
app = xw.App(visible=False)
self.logger.log(f"xlwings 시작", level=logging.DEBUG)
self.logger.log("xlwings 시작", level=logging.DEBUG)
try:
total_rows = len(df)
total_rows = len(filtered_df)
for i in range(0, total_rows, 50):
df_subset = df.iloc[i:i+50]
self.logger.log(f"{i}번째 출력할 데이터:\n{df_subset}", level=logging.DEBUG) # 데이터 검증 로그 추가
df_subset = filtered_df.iloc[i:i+50]
part_file_name = output_path.replace('.xlsx', f'_part{i//50 + 1}.xlsx')
shutil.copy(self.base_excel_path, part_file_name)
self.logger.log(f"기본 엑셀 파일 '{self.base_excel_path}' 복사 완료", level=logging.DEBUG)
wb = xw.Book(part_file_name)
ws = wb.sheets['multi_ss']
# 각 행에 대해 처리
for index, row in df_subset.iterrows():
row_num = 4 + (index % 50)
self.logger.log(f"{index + 1}번째 행 기록 시작: B{row_num}, C{row_num}, D{row_num}, F{row_num}, G{row_num}, H{row_num}", level=logging.DEBUG) # 셀 위치 로그 추가
# 무조건 pc_url 출력 (B열)
ws.range(f'B{row_num}').value = row['pc_url']
ws.range(f'C{row_num}').value = row['generated_Title']
ws.range(f'D{row_num}').value = row['price']
ws.range(f'F{row_num}').value = row['tags']
ws.range(f'G{row_num}').value = row['category_code']
# 기본적으로 memo는 H열에 기록
ws.range(f'H{row_num}').value = row['memo']
# 데이터베이스 업데이트
if options.get("shoppinglens"):
# 쇼핑렌즈 수집이 활성화된 경우 옵션에 따라 다른 컬럼에 기록
if options.get("title_generation"):
ws.range(f'C{row_num}').value = row.get('generated_Title', "")
if options.get("margin_price"):
ws.range(f'D{row_num}').value = row.get('margin_price', "")
if options.get("category_input"):
ws.range(f'G{row_num}').value = row.get('category_code', "")
# DB 업데이트 시에도 해당 컬럼들을 함께 저장합니다.
self.db_manager.update_item({
'id': row['id'],
'generated_Title': row.get('generated_Title', None),
'category_code': row['category_code'],
'tags': row['tags'],
'category_code': row.get('category_code', ""),
'tags': row.get('tags', ""),
'margin_price': row.get('margin_price', None),
'memo': row['memo'],
'is_valid': row['is_valid'],
'is_export': 1 # is_export를 1로 설정
'is_export': 1
})
self.logger.log(f"{index + 1}번째 행 기록 완료", level=logging.DEBUG)
# 프로그레스바 업데이트
progress = int((index + 1) / total_rows * 100)
self.progress_signal.emit(progress)
wb.save(part_file_name) # SaveCopyAs 대신 save 사용
wb.save(part_file_name)
wb.close()
self.saved_files.append(part_file_name)
self.logger.log(f"파일 '{part_file_name}'에 데이터가 저장되었습니다.", level=logging.INFO)
return True # 성공 여부 반환
return True
except Exception as e:
self.logger.log(f"엑셀 저장 중 예외 발생: {e}", level=logging.ERROR, exc_info=True)
return False # 실패 시 False 반환
return False
finally:
app.quit()
self.logger.log(f"xlwings 종료", level=logging.DEBUG)
self.logger.log("xlwings 종료", level=logging.DEBUG)

View File

@ -12,6 +12,7 @@ from src.keyword.keyword_manager import KeywordManager
from src.keyword.kiprisAPI import Kipris_API
from src.categoryManager import CategoryManager
from src.user_info_dialog import UserInfoDialog
from src.toggleSwitch import ToggleSwitch
from login.qr_dialog import QRDialog, LoginSuccessDialog, LoginInProgressDialog
class MainWindow(QWidget):
@ -120,6 +121,50 @@ class MainWindow(QWidget):
self.close_button = QPushButton("닫기")
self.close_button.clicked.connect(self.close)
# 토글레이아웃 추가 (쇼핑렌즈 관련 옵션)
self.toggle_shoppinglens_label = QLabel("쇼핑렌즈 수집")
self.toggle_shoppinglens = ToggleSwitch()
self.toggle_shoppinglens.setToolTip("쇼핑렌즈 데이터를 수집합니다.\n(Free 등급은 사용 불가)")
self.toggle_shoppinglens.setChecked(False)
self.toggle_shoppinglens.stateChanged.connect(self.on_shoppinglens_toggle_changed)
self.toggle_title_generation_label = QLabel("상품명 생성")
self.toggle_title_generation = ToggleSwitch()
self.toggle_title_generation.setToolTip("활성화 시, title_manager.generate_product_name 실행")
self.toggle_title_generation.setChecked(False)
self.toggle_margin_price_label = QLabel("마진계산")
self.toggle_margin_price = ToggleSwitch()
self.toggle_margin_price.setToolTip("활성화 시, calculate_additional_margin 실행")
self.toggle_margin_price.setChecked(False)
self.toggle_category_input_label = QLabel("카테고리 입력")
self.toggle_category_input = ToggleSwitch()
self.toggle_category_input.setToolTip("활성화 시, categoryManager.find_category_code 실행")
self.toggle_category_input.setChecked(False)
# 초기에는 쇼핑렌즈 토글이 꺼져 있으므로 나머지 옵션은 비활성화
self.toggle_title_generation.setEnabled(False)
self.toggle_margin_price.setEnabled(False)
self.toggle_category_input.setEnabled(False)
# 회원 등급에 따라 쇼핑렌즈 토글 활성 여부 결정
membership_level = self.user_info.get('membership_level', '').lower()
if membership_level == "free":
self.toggle_shoppinglens.setEnabled(False)
self.logger.log("회원 등급이 free이므로 쇼핑렌즈 기능 사용 불가", level=logging.INFO)
else:
self.toggle_shoppinglens.setEnabled(True)
# 토글레이아웃에 추가
self.toggle_layout = QHBoxLayout()
self.toggle_layout.addWidget(self.toggle_shoppinglens_label)
self.toggle_layout.addWidget(self.toggle_shoppinglens)
self.toggle_layout.addWidget(self.toggle_title_generation_label)
self.toggle_layout.addWidget(self.toggle_title_generation)
self.toggle_layout.addWidget(self.toggle_margin_price_label)
self.toggle_layout.addWidget(self.toggle_margin_price)
self.toggle_layout.addWidget(self.toggle_category_input_label)
self.toggle_layout.addWidget(self.toggle_category_input)
# 버튼 레이아웃
button_layout = QHBoxLayout()
button_layout.addWidget(self.login_button)
@ -132,12 +177,15 @@ class MainWindow(QWidget):
self.layout.addWidget(self.menu_bar)
self.layout.addLayout(category_layout) # 상품분류 선택 영역 추가
self.layout.addLayout(button_layout)
self.layout.addWidget(QLabel("후처리 옵션"))
self.layout.addLayout(self.toggle_layout)
self.layout.addWidget(QLabel("진행 상황"))
self.layout.addWidget(self.progress_bar)
self.layout.addWidget(QLabel("로그 출력"))
self.layout.addWidget(self.log_text_edit)
self.setLayout(self.layout)
# QR 이미지 표시를 위한 QLabel (초기에는 숨김)
self.qr_label = QLabel()
self.qr_label.setVisible(False)
@ -364,12 +412,12 @@ class MainWindow(QWidget):
self.logger.log(f"기본 선택된 검색어: {self.selected_search_query}", level=logging.DEBUG)
self.subcategory_combo.blockSignals(False)
# # 만약 서브카테고리 콤보박스에 한 번만 연결되어 있지 않다면, 한 번만 연결하도록 처리
# try:
# self.subcategory_combo.currentIndexChanged.disconnect()
# except Exception:
# pass
# self.subcategory_combo.currentIndexChanged.connect(self.update_search_query)
# 만약 서브카테고리 콤보박스에 한 번만 연결되어 있지 않다면, 한 번만 연결하도록 처리
try:
self.subcategory_combo.currentIndexChanged.disconnect()
except Exception:
pass
self.subcategory_combo.currentIndexChanged.connect(self.update_search_query)
def update_search_query(self, index):
major = self.category_combo.currentText()
@ -386,6 +434,9 @@ class MainWindow(QWidget):
def post_process_by_DB(self):
self.logger.log(f"DB로 후처리 작업을 시작합니다.", level=logging.INFO)
self.progress_bar.setValue(1)
# 토글 상태 업데이트 후 옵션 전달
self.update_toggle_options()
self.postProcessor.options = self.toggle_options
self.db_thread = ProcessingThread(self.postProcessor, self.keyword_manager)
self.db_thread.progress.connect(self.on_DB_progress)
self.db_thread.start()
@ -425,6 +476,26 @@ class MainWindow(QWidget):
self.xls_thread.progress.connect(self.on_xls_progress)
self.xls_thread.start()
def on_shoppinglens_toggle_changed(self, state):
# 쇼핑렌즈 토글이 켜져 있어야 나머지 옵션 활성화
enabled = (state == 2)
self.toggle_title_generation.setEnabled(enabled)
self.toggle_margin_price.setEnabled(enabled)
self.toggle_category_input.setEnabled(enabled)
# 토글 상태 업데이트
self.update_toggle_options()
def update_toggle_options(self):
# 현재 각 토글의 상태를 딕셔너리에 저장
self.toggle_options = {
"shoppinglens": self.toggle_shoppinglens.isChecked(),
"title_generation": self.toggle_title_generation.isChecked(),
"margin_price": self.toggle_margin_price.isChecked(),
"category_input": self.toggle_category_input.isChecked()
}
self.logger.log(f"토글 옵션 업데이트: {self.toggle_options}", level=logging.DEBUG)
@Slot(str)
def on_xls_progress(self, message):
self.logger.log(message, level=logging.INFO)
@ -432,7 +503,8 @@ class MainWindow(QWidget):
@Slot()
def save_to_excel(self):
self.progress_bar.setValue(1)
success = self.excel_exporter.save_to_excel()
self.update_toggle_options()
success = self.excel_exporter.save_to_excel(options=self.toggle_options)
if success:
QMessageBox.information(self, "엑셀 출력", "엑셀 파일로 저장 완료")
else:

View File

@ -28,6 +28,8 @@ class PostProcessor(QObject):
self.xlThread = XlsSerachThread(self.logger, self.db_manager)
self.banned_words = None
self.disallowed_words = None
# 기본 옵션 설정 (나중에 MainWindow에서 덮어쓰기 가능)
self.options = {"shoppinglens": False, "title_generation": False, "margin_price": False, "category_input": False}
def set_keyword_manager(self, keyword_manager):
self.banned_words = keyword_manager.get_ban_list()
@ -94,57 +96,6 @@ class PostProcessor(QObject):
self.logger.log(f"URL 데이터 가져오기 오류: {url}, 오류: {e}", level=logging.ERROR, exc_info=True)
return None, None
def process_products(self, products: List[Dict]):
total_products = len(products)
if total_products == 0:
self.logger.log("처리할 상품이 없습니다.", level=logging.WARNING)
self.progress_signal.emit(100)
return
self.wh_con.start_whale_Browser()
for idx, product in enumerate(products, start=1):
try:
self.logger.log(f"상품 {product['product_id']} 쇼핑렌즈 검색 시작", level=logging.DEBUG)
scraped_data = self.wh_con.search_and_parse(image_url=product['image_url'])
if not scraped_data:
self.logger.log(f"상품 {product['product_id']} 쇼핑렌즈 데이터 없음, 스킵", level=logging.WARNING)
continue
most_common_category = self.categoryManager.find_most_common_category(scraped_data)
self.logger.log(f"결정된 카테고리: {most_common_category}", level=logging.DEBUG)
category_code = self.categoryManager.find_category_code(most_common_category)
self.logger.log(f"결정된 카테고리 코드: {category_code}", level=logging.DEBUG)
if scraped_data:
isvalid_category = self.categoryManager.is_category_allowed(most_common_category)
self.logger.log(f"isvalid_category: {isvalid_category}", level=logging.DEBUG)
product['is_valid'] = 1 if isvalid_category else 0
titles = [item["title"] for item in scraped_data if "title" in item]
final_title = self.title_manager.generate_product_name(titles, product['name'], self.banned_words, self.disallowed_words)
self.logger.log(f"상품명 생성 완료: {final_title}", level=logging.DEBUG)
tags = self.filter_and_merge_tags(scraped_data)
additional_margin = self.calculate_additional_margin(scraped_data)
self.logger.log(f"더하기 마진: {additional_margin}", level=logging.DEBUG)
product.update({
"generated_Title": final_title,
"category_code": category_code,
"tags": tags,
"margin_price": additional_margin,
"memo": self.generate_memo(scraped_data)
})
self.db_manager.update_item(product)
self.logger.log(f"상품 {product['product_id']} 처리 완료", level=logging.DEBUG)
progress = int((idx / total_products) * 100)
self.progress_signal.emit(progress)
except Exception as e:
self.logger.log(f"상품 {product['product_id']} 처리 중 오류: {e}", level=logging.ERROR, exc_info=True)
self.progress_signal.emit(100)
self.logger.log("모든 상품 처리가 완료되었습니다.", level=logging.INFO)
def filter_and_merge_tags(self, scraped_data) -> str:
try:
manu_tags = [item.get("manuTag") for item in scraped_data if item.get("manuTag")]
@ -185,6 +136,130 @@ class PostProcessor(QObject):
self.logger.log(f"메모 생성 오류: {e}", level=logging.ERROR, exc_info=True)
return "메모 없음"
# def process_products(self, products: List[Dict]):
# total_products = len(products)
# if total_products == 0:
# self.logger.log("처리할 상품이 없습니다.", level=logging.WARNING)
# self.progress_signal.emit(100)
# return
# self.wh_con.start_whale_Browser()
# for idx, product in enumerate(products, start=1):
# try:
# self.logger.log(f"상품 {product['product_id']} 쇼핑렌즈 검색 시작", level=logging.DEBUG)
# scraped_data = self.wh_con.search_and_parse(image_url=product['image_url'])
# if not scraped_data:
# self.logger.log(f"상품 {product['product_id']} 쇼핑렌즈 데이터 없음, 스킵", level=logging.WARNING)
# continue
# most_common_category = self.categoryManager.find_most_common_category(scraped_data)
# self.logger.log(f"결정된 카테고리: {most_common_category}", level=logging.DEBUG)
# category_code = self.categoryManager.find_category_code(most_common_category)
# self.logger.log(f"결정된 카테고리 코드: {category_code}", level=logging.DEBUG)
# if scraped_data:
# isvalid_category = self.categoryManager.is_category_allowed(most_common_category)
# self.logger.log(f"isvalid_category: {isvalid_category}", level=logging.DEBUG)
# product['is_valid'] = 1 if isvalid_category else 0
# titles = [item["title"] for item in scraped_data if "title" in item]
# final_title = self.title_manager.generate_product_name(titles, product['name'], self.banned_words, self.disallowed_words)
# self.logger.log(f"상품명 생성 완료: {final_title}", level=logging.DEBUG)
# tags = self.filter_and_merge_tags(scraped_data)
# additional_margin = self.calculate_additional_margin(scraped_data)
# self.logger.log(f"더하기 마진: {additional_margin}", level=logging.DEBUG)
# product.update({
# "generated_Title": final_title,
# "category_code": category_code,
# "tags": tags,
# "margin_price": additional_margin,
# "memo": self.generate_memo(scraped_data)
# })
# self.db_manager.update_item(product)
# self.logger.log(f"상품 {product['product_id']} 처리 완료", level=logging.DEBUG)
# progress = int((idx / total_products) * 100)
# self.progress_signal.emit(progress)
# except Exception as e:
# self.logger.log(f"상품 {product['product_id']} 처리 중 오류: {e}", level=logging.ERROR, exc_info=True)
# self.progress_signal.emit(100)
# self.logger.log("모든 상품 처리가 완료되었습니다.", level=logging.INFO)
def process_products(self, products: List[Dict]):
total_products = len(products)
if total_products == 0:
self.logger.log("처리할 상품이 없습니다.", level=logging.WARNING)
self.progress_signal.emit(100)
return
self.wh_con.start_whale_Browser()
for idx, product in enumerate(products, start=1):
try:
# 만약 쇼핑렌즈 수집 옵션이 비활성화되었다면 후처리 작업을 생략합니다.
if not self.options.get("shoppinglens"):
self.logger.log(f"상품 {product['product_id']} 후처리 생략 (쇼핑렌즈 수집 비활성)", level=logging.DEBUG)
continue
self.logger.log(f"상품 {product['product_id']} 쇼핑렌즈 검색 시작", level=logging.DEBUG)
scraped_data = self.wh_con.search_and_parse(image_url=product['image_url'])
if not scraped_data:
self.logger.log(f"상품 {product['product_id']} 쇼핑렌즈 데이터 없음, 스킵", level=logging.WARNING)
continue
most_common_category = self.categoryManager.find_most_common_category(scraped_data)
self.logger.log(f"결정된 카테고리: {most_common_category}", level=logging.DEBUG)
# 카테고리 입력 옵션에 따라 처리
category_code = ""
if self.options.get("category_input"):
category_code = self.categoryManager.find_category_code(most_common_category)
self.logger.log(f"결정된 카테고리 코드: {category_code}", level=logging.DEBUG)
# 기본적으로 유효성 체크
if scraped_data:
isvalid_category = self.categoryManager.is_category_allowed(most_common_category)
self.logger.log(f"isvalid_category: {isvalid_category}", level=logging.DEBUG)
product['is_valid'] = 1 if isvalid_category else 0
# 상품명 생성 옵션
final_title = ""
if self.options.get("title_generation"):
titles = [item["title"] for item in scraped_data if "title" in item]
final_title = self.title_manager.generate_product_name(titles, product['name'], self.banned_words, self.disallowed_words)
self.logger.log(f"상품명 생성 완료: {final_title}", level=logging.DEBUG)
# 마진 계산 옵션
additional_margin = 0
if self.options.get("margin_price"):
additional_margin = self.calculate_additional_margin(scraped_data)
self.logger.log(f"추가 마진 계산 결과: {additional_margin}", level=logging.DEBUG)
tags = self.filter_and_merge_tags(scraped_data)
product.update({
"generated_Title": final_title,
"category_code": category_code,
"tags": tags,
"margin_price": additional_margin,
"memo": self.generate_memo(scraped_data)
})
self.db_manager.update_item(product)
self.logger.log(f"상품 {product['product_id']} 처리 완료", level=logging.DEBUG)
progress = int((idx / total_products) * 100)
self.progress_signal.emit(progress)
except Exception as e:
self.logger.log(f"상품 {product['product_id']} 처리 중 오류: {e}", level=logging.ERROR, exc_info=True)
self.progress_signal.emit(100)
self.logger.log("모든 상품 처리가 완료되었습니다.", level=logging.INFO)
# 독립 실행 시 (테스트용)
# if __name__ == "__main__":
# from src.loggerModule import Logger

188
src/toggleSwitch.py Normal file
View File

@ -0,0 +1,188 @@
from PySide6.QtCore import Qt, QRect, QPropertyAnimation, Property, Signal, QPoint
from PySide6.QtGui import QPainter, QColor, QFont
from PySide6.QtWidgets import QWidget, QLabel
class ToggleSwitch(QWidget):
"""
범용 토글 스위치 위젯
QCheckBox와 유사한 인터페이스를 제공합니다.
"""
# clicked 시그널 (bool 타입)
clicked = Signal(bool)
# QCheckBox 호환용 stateChanged 시그널 (int 타입)
# QCheckBox는 stateChanged에 0/2를 보내므로 변환합니다.
stateChanged = Signal(int)
THEMES = {
"default": (QColor("#CCCCCC"), QColor("#00B16A"), QColor("#E6E6E6")),
"dark": (QColor("#444444"), QColor("#00B16A"), QColor("#888888")),
"light": (QColor("#FFFFFF"), QColor("#007ACC"), QColor("#CCCCCC")),
"blue": (QColor("#D0E7FF"), QColor("#005F99"), QColor("#A6C8FF")),
}
def __init__(self, label: str = "", parent=None, width=50, height=25, animation_duration=250, theme="default"):
super().__init__(parent)
self._width = width
self._height = height
self.setFixedSize(self._width, self._height)
self._checked = False
self._animation_duration = animation_duration
self._background_color = QColor("#CCCCCC")
self._circle_color_checked = QColor("#00B16A")
self._circle_color_unchecked = QColor("#E6E6E6")
self.setTheme(theme)
self._circle_diameter = self._height
self._circle_pos = QPoint(0, 0)
self.animation = QPropertyAnimation(self, b"circle_pos")
self.animation.setDuration(self._animation_duration)
self._init_position()
# QCheckBox 호환: stateChanged 시그널은 클릭시 clicked 신호를 int형으로 변환하여 방출
# (checked=True -> 2, False -> 0)
# 아래처럼 clicked 시그널에 연결하여 stateChanged도 함께 방출하도록 합니다.
self.clicked.connect(lambda state: self.stateChanged.emit(2 if state else 0))
# 라벨 텍스트 (선택 사항)
self.label = label
if self.label:
self._label_widget = QLabel(self.label, self)
self._label_widget.setAlignment(Qt.AlignCenter)
font = QFont()
font.setPointSize(10)
self._label_widget.setFont(font)
self._label_widget.setStyleSheet("background: transparent;")
self._label_widget.setGeometry(0, 0, self._width, self._height)
else:
self._label_widget = None
@Property(QPoint)
def circle_pos(self):
return self._circle_pos
@circle_pos.setter
def circle_pos(self, pos):
self._circle_pos = pos
self.update()
def _init_position(self):
if self._checked:
self._circle_pos.setX(self._width - self._circle_diameter)
else:
self._circle_pos.setX(0)
self._circle_pos.setY(0)
def setTheme(self, theme_name):
if isinstance(theme_name, str):
theme = self.THEMES.get(theme_name, self.THEMES["default"])
self._background_color, self._circle_color_checked, self._circle_color_unchecked = theme
elif isinstance(theme_name, dict):
self._background_color = theme_name.get("background", self._background_color)
self._circle_color_checked = theme_name.get("checked", self._circle_color_checked)
self._circle_color_unchecked = theme_name.get("unchecked", self._circle_color_unchecked)
else:
self._background_color, self._circle_color_checked, self._circle_color_unchecked = self.THEMES["default"]
self.update()
def setAnimationDuration(self, duration):
self._animation_duration = duration
self.animation.setDuration(self._animation_duration)
def mousePressEvent(self, event):
if event.button() == Qt.LeftButton:
self._checked = not self._checked
self.clicked.emit(self._checked)
self._update_animation()
self.update()
super().mousePressEvent(event)
def _update_animation(self):
if self._checked:
start = QPoint(0, 0)
end = QPoint(self._width - self._circle_diameter, 0)
else:
start = QPoint(self._width - self._circle_diameter, 0)
end = QPoint(0, 0)
self.animation.stop()
self.animation.setStartValue(start)
self.animation.setEndValue(end)
self.animation.start()
def paintEvent(self, event):
painter = QPainter(self)
painter.setRenderHint(QPainter.Antialiasing)
painter.setPen(Qt.NoPen)
# 위젯이 활성화(enabled) 상태인지 확인
if not self.isEnabled():
# 비활성화 상태일 때는 배경색과 원의 색상을 어둡게 처리
background = self._background_color.darker(150)
circle_color = (self._circle_color_checked if self._checked else self._circle_color_unchecked).darker(150)
else:
background = self._background_color
circle_color = self._circle_color_checked if self._checked else self._circle_color_unchecked
# 배경 그리기
painter.setBrush(background)
painter.drawRoundedRect(QRect(0, 0, self._width, self._height), self._height / 2, self._height / 2)
# 원(circle) 그리기
painter.setBrush(circle_color)
painter.drawEllipse(self._circle_pos.x(), self._circle_pos.y(), self._circle_diameter, self._circle_diameter)
def setChecked(self, checked: bool):
if self._checked != checked:
self._checked = checked
self._update_animation()
self.update()
def isChecked(self) -> bool:
return self._checked
def setState(self, state: bool):
if self._checked != state:
self._checked = state
self._update_animation()
self.clicked.emit(self._checked)
self.update()
def toggle(self):
self.setState(not self._checked)
# 사용 예시
# from PySide6.QtWidgets import QApplication, QVBoxLayout, QWidget
# from toggle_switch import ToggleSwitch
# import sys
# app = QApplication(sys.argv)
# window = QWidget()
# layout = QVBoxLayout(window)
# # 기본 테마 (default)
# toggle1 = ToggleSwitch()
# toggle1.clicked.connect(lambda state: print("Toggle 1:", state))
# layout.addWidget(toggle1)
# # 다크 테마, 크기 및 애니메이션 지속시간 변경
# toggle2 = ToggleSwitch(width=60, height=30, animation_duration=300, theme="dark")
# toggle2.clicked.connect(lambda state: print("Toggle 2:", state))
# layout.addWidget(toggle2)
# # 사용자 지정 테마
# custom_theme = {
# "background": QColor("#F0F0F0"),
# "checked": QColor("#FF5722"),
# "unchecked": QColor("#BDBDBD")
# }
# toggle3 = ToggleSwitch(theme=custom_theme)
# toggle3.clicked.connect(lambda state: print("Toggle 3:", state))
# layout.addWidget(toggle3)
# window.show()
# sys.exit(app.exec())

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 29 KiB