Mycar_SMS_Sender2/gui/settings_dialog.py

415 lines
17 KiB
Python

# gui/settings_dialog.py
from PySide6.QtWidgets import (
QDialog, QVBoxLayout, QHBoxLayout, QLineEdit, QPushButton,
QListWidget, QLabel, QMessageBox, QTabWidget, QWidget, QComboBox
)
from PySide6.QtCore import Qt, QEvent, Signal
import logging
class ShopTagDialog(QDialog):
"""
샵 이름 추가 시, 사용 가능한 태그(숫자) 중 하나를 선택하는 다이얼로그.
available_tags에는 사용되지 않은 태그 숫자 리스트가 전달됩니다.
"""
def __init__(self, available_tags, parent=None):
super().__init__(parent)
self.setWindowTitle("샵 태그 선택")
self.selected_tag = None
layout = QVBoxLayout(self)
label = QLabel("이 샵의 태그 번호를 선택하세요:")
layout.addWidget(label)
self.combo = QComboBox()
self.combo.setEditable(True)
# available_tags가 예: [3, 4, 5, 6, 7]라면 콤보박스에는 "3사", "4사", ... 로 표시
for tag in available_tags:
self.combo.addItem(f"{tag}", tag)
layout.addWidget(self.combo)
# 엔터키를 누르면 다이얼로그를 수락
if self.combo.lineEdit() is not None:
self.combo.lineEdit().returnPressed.connect(self.accept)
button_layout = QHBoxLayout()
ok_button = QPushButton("확인")
cancel_button = QPushButton("취소")
ok_button.clicked.connect(self.accept)
cancel_button.clicked.connect(self.reject)
button_layout.addWidget(ok_button)
button_layout.addWidget(cancel_button)
layout.addLayout(button_layout)
def exec_(self):
result = super().exec_()
if result == QDialog.Accepted:
self.selected_tag = self.combo.currentData()
return result
class setListWidget(QListWidget):
# DEL 키 입력 시 발생시키는 커스텀 시그널
deletePressed = Signal()
def keyPressEvent(self, event):
if event.key() == Qt.Key_Delete:
# DEL 키가 눌리면 시그널을 emit하고 기본 동작(선택 해제 등)은 하지 않음.
self.deletePressed.emit()
event.accept()
else:
super().keyPressEvent(event)
class SettingsDialog(QDialog):
def __init__(self, logger, db_manager, parent=None):
"""
:param logger: 로깅 객체
:param db_manager: DatabaseManager 인스턴스 (database_module.py의 DatabaseManager)
"""
super().__init__(parent)
self.setWindowTitle("사용자 설정")
self.resize(600, 500)
self.logger = logger
self.db_manager = db_manager
# 관리할 항목: key는 DB상의 카테고리명, value는 표시될 이름
self.categories = {
"sms_quota": "문자 할당량 설정",
"shop_names": "샵 이름",
"order_markets": "주문마켓",
"domestic_couriers": "국내 택배사",
"cargo_couriers": "화물 택배사"
}
# 탭 순서를 보존하기 위해 key 목록 저장
self.tab_keys = list(self.categories.keys())
self.widgets = {}
self.setup_ui()
self.load_all_settings()
self.setStyleSheet(self.load_style_sheet())
# 탭 변경 시 해당 탭의 입력창에 포커스하도록 연결
self.tab_widget.currentChanged.connect(self.on_tab_changed)
def setup_ui(self):
main_layout = QVBoxLayout(self)
self.tab_widget = QTabWidget()
for key, display_name in self.categories.items():
if key == "sms_quota":
self.setup_sms_quota_tab()
else:
tab = QWidget()
tab_layout = QVBoxLayout(tab)
# 저장된 항목을 표시하는 리스트 위젯
list_widget = setListWidget()
list_widget.setToolTip(f"저장된 {display_name} 목록을 확인하세요.")
# 리스트 위젯에 이벤트 필터 설치 (DEL 키 처리용)
list_widget.installEventFilter(self)
tab_layout.addWidget(list_widget)
# 입력창과 추가 버튼을 담는 수평 레이아웃
input_layout = QHBoxLayout()
line_edit = QLineEdit()
line_edit.setPlaceholderText(f"{display_name} 입력")
line_edit.returnPressed.connect(lambda cat=key: self.add_item(cat))
line_edit.setToolTip(f"새로운 {display_name}을(를) 입력하세요.")
input_layout.addWidget(line_edit)
add_button = QPushButton("추가")
add_button.setToolTip(f"{display_name}을(를) 추가합니다.")
add_button.clicked.connect(lambda _, cat=key: self.add_item(cat))
input_layout.addWidget(add_button)
tab_layout.addLayout(input_layout)
# 엔터키로도 추가 (샵 이름의 경우)
if key == "shop_names":
line_edit.returnPressed.connect(lambda cat=key: self.add_item(cat))
# 삭제 버튼
delete_button = QPushButton("삭제")
delete_button.setToolTip(f"선택한 {display_name}을(를) 삭제합니다.")
delete_button.clicked.connect(lambda _, cat=key: self.delete_item(cat))
tab_layout.addWidget(delete_button, alignment=Qt.AlignRight)
self.tab_widget.addTab(tab, display_name)
index = self.tab_widget.indexOf(tab)
self.tab_widget.setTabToolTip(index, f"{display_name} 설정을 관리합니다.")
self.widgets[key] = {
"list": list_widget,
"input": line_edit,
"add": add_button,
"delete": delete_button
}
main_layout.addWidget(self.tab_widget)
# 닫기 버튼
button_layout = QHBoxLayout()
close_button = QPushButton("닫기")
close_button.setToolTip("설정 창을 닫습니다.")
close_button.clicked.connect(self.accept)
button_layout.addWidget(close_button)
main_layout.addLayout(button_layout)
def eventFilter(self, obj, event):
# 리스트 위젯에서 Delete 키 누르면 삭제 처리
if event.type() == QEvent.KeyPress and event.key() == Qt.Key_Delete:
# widgets 딕셔너리에서 해당 리스트 위젯을 찾습니다.
for category, widget_dict in self.widgets.items():
if widget_dict.get("list") == obj:
self.delete_item(category)
return True
return super().eventFilter(obj, event)
def setup_sms_quota_tab(self):
""" 문자 할당량 설정 탭 구성 """
tab = QWidget()
layout = QVBoxLayout(tab)
layout.addWidget(QLabel("통신사 선택:"))
self.carrier_combo = QComboBox()
self.load_carriers_into_combobox()
self.carrier_combo.setEditable(False)
self.carrier_combo.currentIndexChanged.connect(self.update_charge_display)
layout.addWidget(self.carrier_combo)
layout.addWidget(QLabel("한달 문자 할당량 (건):"))
self.quota_input = QLineEdit()
self.quota_input.setPlaceholderText("예: 100")
layout.addWidget(self.quota_input)
layout.addWidget(QLabel("초과 시 건당 과금 금액 (원):"))
self.charge_label = QLabel("SMS: 0 원 / MMS: 0 원") # 과금 금액을 표시할 QLabel
layout.addWidget(self.charge_label)
save_button = QPushButton("저장")
save_button.clicked.connect(self.save_sms_quota)
layout.addWidget(save_button)
self.tab_widget.addTab(tab, "문자 할당량 설정")
# 저장 값 로드
self.load_sms_quota()
def load_carriers_into_combobox(self):
""" DB에서 통신사 목록을 불러와 QComboBox에 추가 """
self.carrier_combo.clear() # 기존 목록 초기화
carriers = self.db_manager.get_all_carriers()
if not carriers:
self.logger.log("DB에서 통신사 목록을 가져올 수 없습니다.", level=logging.ERROR, exc_info=True)
return
for carrier in carriers:
self.carrier_combo.addItem(carrier.service_name, (carrier.sms_charge, carrier.mms_charge))
def update_charge_display(self):
""" 통신사 선택 시 해당 통신사의 SMS 과금 금액을 QLabel에 표시 """
charges = self.carrier_combo.currentData()
if charges is not None:
sms_charge, mms_charge = charges
self.charge_label.setText(f"SMS: {sms_charge} 원 / MMS: {mms_charge}")
def set_default_values(self):
""" 기본값 설정: 통신사는 'SK Telecom', 할당량은 100건 """
index = self.carrier_combo.findText("SK텔레콤")
if index != -1:
self.carrier_combo.setCurrentIndex(index)
self.quota_input.setText("100")
self.update_charge_display()
def load_sms_quota(self):
""" 저장된 문자 할당량 및 통신사 정보를 불러옴 (항상 1개만 유지) """
saved_quotas = self.db_manager.get_settings_by_category("sms_quota")
if saved_quotas:
# 가장 최근에 저장된 값을 가져옴
saved_quota = saved_quotas[-1] # 마지막 항목이 가장 최신 값
carrier, quota = saved_quota.split(":")
self.logger.log(f"저장된 문자 할당량 로드: {carrier}, {quota}", level=logging.DEBUG)
# 콤보박스에서 해당 통신사를 선택
index = self.carrier_combo.findText(carrier)
if index != -1:
self.carrier_combo.setCurrentIndex(index)
self.quota_input.setText(quota)
self.update_charge_display()
else:
self.logger.log("저장된 문자 할당량이 없습니다. 기본값 설정", level=logging.DEBUG)
self.set_default_values()
def save_sms_quota(self):
""" 기존 문자 할당량 설정을 삭제한 후, 새 값을 저장 """
carrier = self.carrier_combo.currentText().strip()
try:
quota = int(self.quota_input.text().strip())
# 기존 sms_quota 설정 삭제
self.db_manager.delete_setting_for_sms_quota("sms_quota")
# 새 sms_quota 값 저장
self.db_manager.add_setting("sms_quota", f"{carrier}:{quota}")
QMessageBox.information(self, "저장 완료", f"{carrier}의 문자 할당량이 저장되었습니다.")
except ValueError:
QMessageBox.warning(self, "입력 오류", "할당량은 숫자로 입력해야 합니다.")
def on_tab_changed(self, index):
key = self.tab_keys[index]
if key in self.widgets and "input" in self.widgets[key]:
self.widgets[key]["input"].setFocus()
def load_all_settings(self):
"""
각 카테고리별로 DB에 저장된 값을 읽어와 리스트 위젯에 표시합니다.
샵 이름의 경우, DB에 별도 저장된 tag와 value를 "[tag사업자] shop_name" 형식으로 표시합니다.
"""
for category in self.categories.keys():
if category == "sms_quota":
continue # 문자 할당량 설정은 별도로 로드
self.widgets[category]["list"].clear()
if category == "shop_names":
records = self.db_manager.get_settings_by_category(category)
# records는 각 레코드가 tag와 value 속성을 가지고 있다고 가정합니다.
for rec in records:
display_text = f"[{rec.tag}사업자] {rec.value}"
self.widgets[category]["list"].addItem(display_text)
else:
values = self.db_manager.get_settings_by_category(category)
for item in values:
self.widgets[category]["list"].addItem(item)
def get_available_shop_tags(self, used):
"""
이미 사용 중인 태그 집합을 받아, 사용되지 않은 숫자 중 _최소 5개_를 반환합니다.
예를 들어 used가 {1, 2}라면 [3, 4, 5, 6, 7]를 반환합니다.
:param used: set(int) 이미 사용된 태그 번호
:return: list(int) 사용 가능한 태그 번호 (최소 5개)
"""
available = []
num = 1
while len(available) < 5:
if num not in used:
available.append(num)
num += 1
return available
def add_item(self, category):
"""
지정된 카테고리에 새 항목을 추가합니다.
샵 이름의 경우 태그 선택 다이얼로그를 통해 tag를 선택하고, DB에는 별도의 tag 필드와 함께 저장합니다.
"""
display_name = self.categories.get(category, category)
text = self.widgets[category]["input"].text().strip()
if not text:
return
if category == "shop_names":
# 이미 등록된 샵 이름(및 태그)을 조회 (DB에서 tag와 value로 관리)
existing = self.db_manager.get_settings_by_category("shop_names")
used_tags = set()
for rec in existing:
if rec.tag is not None:
used_tags.add(rec.tag)
if rec.value.strip().lower() == text.lower():
QMessageBox.warning(self, "중복", "이미 존재하는 샵 이름입니다.")
return
available = self.get_available_shop_tags(used_tags)
tag_dialog = ShopTagDialog(available, self)
if tag_dialog.exec_() == QDialog.Accepted:
chosen_tag = tag_dialog.selected_tag
else:
return # 취소 시 중단
# DB에 추가: add_shop_setting(category, value, tag)
success = self.db_manager.add_shop_setting("shop_names", text, chosen_tag)
if not success:
QMessageBox.warning(self, "중복", f"이미 존재하는 {display_name}입니다.")
return
else:
final_value = text
success = self.db_manager.add_setting(category, final_value)
if not success:
QMessageBox.warning(self, "중복", f"이미 존재하는 {display_name}입니다.")
return
self.load_all_settings()
self.widgets[category]["input"].clear()
def delete_item(self, category):
"""
지정된 카테고리에서 선택한 항목을 삭제합니다.
샵 이름의 경우 "[tag사] shop_name"에서 tag와 shop_name을 추출하여 삭제합니다.
"""
selected_items = self.widgets[category]["list"].selectedItems()
if not selected_items:
return
for item in selected_items:
if category == "shop_names":
text = item.text()
if text.startswith("[") and "]" in text:
try:
tag_str = text.split("]")[0][1:] # 예: "[3사" -> "3사"
# tag_str이 "3사"이면 tag를 정수로 변환
tag_num = int(tag_str.replace("사업자", ""))
shop_name = text.split("]", 1)[1].strip()
self.db_manager.delete_shop_setting(category, shop_name, tag_num)
except Exception as e:
self.logger.log(f"샵 이름 삭제 에러: {e}", level=logging.ERROR, exc_info=True)
else:
self.db_manager.delete_setting(category, item.text())
self.load_all_settings()
def load_style_sheet(self):
"""
현대적이고 모던한 느낌의 스타일 시트를 반환합니다.
"""
return """
QDialog {
background-color: #f7f9fc;
font-family: 'Segoe UI', sans-serif;
font-size: 10pt;
}
QTabWidget::pane {
border: 1px solid #dcdcdc;
background: #ffffff;
}
QTabBar::tab {
background: #e8eff7;
border: 1px solid #dcdcdc;
padding: 8px;
border-top-left-radius: 4px;
border-top-right-radius: 4px;
margin-right: 2px;
min-width: 100px;
}
QTabBar::tab:selected {
background: #ffffff;
border-bottom: 2px solid #3498db;
font-weight: bold;
}
QLineEdit {
border: 1px solid #dcdcdc;
padding: 6px;
border-radius: 4px;
background-color: #ffffff;
}
QPushButton {
background-color: #3498db;
color: #ffffff;
border: none;
padding: 6px 12px;
border-radius: 4px;
}
QPushButton:hover {
background-color: #2980b9;
}
QListWidget {
background: #ffffff;
border: 1px solid #dcdcdc;
padding: 4px;
border-radius: 4px;
}
"""