Mycar_SMS_Sender2/gui/main_window.py

475 lines
20 KiB
Python

# gui/main_window.py
from datetime import datetime
from PySide6.QtWidgets import (
QMainWindow, QWidget, QVBoxLayout, QHBoxLayout,
QPushButton, QTextEdit, QMenuBar, QMenu, QLabel,
QTableWidget, QTableWidgetItem, QMessageBox, QDialog, QLineEdit
)
from PySide6.QtGui import QAction, QPixmap, QFont
from PySide6.QtCore import Qt, Slot, QThread, QSettings, QTimer, QCoreApplication
from gui.order_input_dialog import OrderInputDialog
from gui.template_management_dialog import TemplateManagementDialog
from gui.help_dialog import HelpDialog
from gui.settings_dialog import SettingsDialog
from src.database_module import DatabaseManager
from src.sms_worker import SMSMessengerWorker
from src.logger_module import Logger
import logging
import asyncio
class MainWindow(QMainWindow):
def __init__(self, loop):
super().__init__()
self.logger = Logger(gui_logger=self.append_log, log_file="SMS_Sender.log", logger_name="SMS_Sender_Logger", level=logging.INFO)
self.settings = QSettings("When_Ride_Mycar", "SMS_Sender")
self.db_manager = DatabaseManager(self.logger) # SQLite, SQLAlchemy 기반 DB 관리자
self.setWindowTitle("내차는 언제타냐!! 주문 알림 SMS 전송 프로그램")
self.resize(900, 700)
self.setup_menu()
self.setup_ui()
self.apply_styles()
self.loop = loop
# 매달 1일(또는 저장된 월과 현재 월이 다르면) SMS 카운트 초기화
self.check_and_reset_sms_count()
self.update_sms_count_label()
self.refresh_order_list()
# SMSMessengerWorker 생성 (async 방식)
self.sms_worker = SMSMessengerWorker(self.logger, headless=True, delay=1)
self.qr_dialog = None
# 시그널 연결: Worker에서 QR 코드 감지 및 로그인 성공 시 UI 업데이트
self.sms_worker.qrCodeFound.connect(self.show_qr_dialog)
self.sms_worker.loginSuccess.connect(self.handle_login_success)
# 이벤트 루프가 실행된 후에 worker의 run()를 예약
QTimer.singleShot(0, lambda: asyncio.create_task(self.sms_worker.run()))
def setup_menu(self):
menu_bar = self.menuBar()
file_menu = menu_bar.addMenu("파일")
settings_action = QAction("사용자 설정", self)
settings_action.triggered.connect(self.open_settings_dialog)
file_menu.addAction(settings_action)
exit_action = QAction("종료", self)
exit_action.triggered.connect(self.close)
file_menu.addAction(exit_action)
help_menu = menu_bar.addMenu("도움말")
usage_action = QAction("프로그램 사용설명서", self)
usage_action.triggered.connect(self.show_help)
help_menu.addAction(usage_action)
def setup_ui(self):
central_widget = QWidget(self)
self.setCentralWidget(central_widget)
main_layout = QVBoxLayout(central_widget)
# 상단 버튼 영역
button_layout = QHBoxLayout()
self.order_input_button = QPushButton("주문정보 입력")
self.order_input_button.setToolTip("주문 정보를 입력하여 문자 전송 준비")
self.order_input_button.clicked.connect(self.open_order_input_dialog)
button_layout.addWidget(self.order_input_button)
self.template_button = QPushButton("템플릿 관리")
self.template_button.setToolTip("문자 템플릿을 저장, 불러오기 및 수정")
self.template_button.clicked.connect(self.open_template_management_dialog)
button_layout.addWidget(self.template_button)
# 로그인 버튼 추가
self.login_button = QPushButton("🔑 로그인")
self.login_button.setToolTip("Google Messages에 로그인합니다.")
self.login_button.clicked.connect(self.start_login)
button_layout.addWidget(self.login_button)
# SMS 전송 버튼 추가
self.send_sms_button = QPushButton("📩 SMS 전송 테스트")
self.send_sms_button.setToolTip("테스트 SMS를 전송합니다.")
self.send_sms_button.clicked.connect(self.send_test_sms)
button_layout.addWidget(self.send_sms_button)
main_layout.addLayout(button_layout)
# SMS 발송 건수 표시 (예: "이번달 총 발송건수: X 건")
self.sms_count_label = QLabel()
self.sms_count_label.setToolTip("이번달 총 SMS 발송 건수")
main_layout.addWidget(self.sms_count_label)
# 주문 목록 테이블 (열 수를 8로 설정)
self.order_table = QTableWidget()
self.order_table.setColumnCount(8)
self.order_table.setHorizontalHeaderLabels([
"ID", "상품명", "고객명", "전화번호", "현재단계", "SMS발송여부", "지금발송", "최종 업데이트"
])
self.order_table.setToolTip("현재 진행 중인 주문 목록을 표시합니다.")
main_layout.addWidget(QLabel("주문 목록:"))
main_layout.addWidget(self.order_table)
# 로그 출력 영역
self.log_display = QTextEdit()
self.log_display.setReadOnly(True)
self.log_display.setToolTip("프로그램 로그가 표시됩니다.")
main_layout.addWidget(QLabel("로그:"))
main_layout.addWidget(self.log_display)
# self.logger.log_signal.connect(self.append_log)
self.order_table.itemChanged.connect(self.item_changed_slot)
@Slot(str)
def append_log(self, message: str):
self.log_display.append(message)
def start_login(self):
self.logger.log("Google Messages 로그인 시작", level=logging.INFO)
asyncio.create_task(self.sms_worker.do_login())
def send_test_sms(self):
dialog = SMSInputDialog(self)
if dialog.exec() == QDialog.Accepted:
recipient = dialog.recipient_edit.text().strip()
message = dialog.message_edit.toPlainText().strip()
if not recipient or not message:
QMessageBox.warning(self, "입력 오류", "전화번호와 메시지를 모두 입력하세요.")
return
sms_request = {"recipient": recipient, "message": message}
asyncio.create_task(self.sms_worker._handle_send_sms(sms_request))
# 또는 sendSMS 시그널을 사용:
# self.sms_worker.sendSMS.emit(sms_request)
def open_order_input_dialog(self):
dialog = OrderInputDialog(self.logger, self.db_manager, parent=self)
if dialog.exec():
self.refresh_order_list()
def open_template_management_dialog(self):
dialog = TemplateManagementDialog(self.logger, self.db_manager, parent=self)
dialog.exec()
def open_settings_dialog(self):
dialog = SettingsDialog(self.logger, self.db_manager)
dialog.exec()
def show_help(self):
dialog = HelpDialog(parent=self)
dialog.exec()
def check_and_reset_sms_count(self):
"""현재 월과 저장된 월을 비교하여, 달이 변경되었으면 SMS 카운트를 초기화"""
current_month = datetime.now().strftime("%Y-%m")
stored_month = self.settings.value("sms_count_month", "")
if stored_month != current_month:
# 새로운 달이 시작되었으므로 카운트를 0으로 초기화하고 저장
self.settings.setValue("sms_count", 0)
self.settings.setValue("sms_count_month", current_month)
def update_sms_count_label(self):
"""SMS 카운트 라벨 업데이트"""
count = self.settings.value("sms_count", 0, type=int)
self.sms_count_label.setText(f"이번달 총 발송건수: {count}")
if count > 0:
self.sms_count_label.setStyleSheet("color: green;")
def increment_sms_count(self):
"""SMS 전송 성공 시 카운트를 1 증가시키고 라벨 업데이트"""
count = self.settings.value("sms_count", 0, type=int)
count += 1
self.settings.setValue("sms_count", count)
self.update_sms_count_label()
def refresh_order_list(self):
orders = self.db_manager.get_all_orders()
self.order_table.blockSignals(True) # 초기화 시 itemChanged 시그널 방지
self.order_table.setRowCount(len(orders))
for row, order in enumerate(orders):
# ID (편집 불가)
id_item = QTableWidgetItem(str(order.id))
id_item.setFlags(id_item.flags() & ~Qt.ItemIsEditable)
self.order_table.setItem(row, 0, id_item)
# 상품명 (편집 가능)
product_item = QTableWidgetItem(order.product_name or "")
product_item.setFlags(product_item.flags() | Qt.ItemIsEditable)
self.order_table.setItem(row, 1, product_item)
# 고객명 (편집 가능)
name_item = QTableWidgetItem(order.customer_name or "")
name_item.setFlags(name_item.flags() | Qt.ItemIsEditable)
self.order_table.setItem(row, 2, name_item)
# 전화번호 (편집 가능)
phone_item = QTableWidgetItem(order.customer_phone)
phone_item.setFlags(phone_item.flags() | Qt.ItemIsEditable)
self.order_table.setItem(row, 3, phone_item)
# 현재단계 (편집 가능)
step_item = QTableWidgetItem(str(order.order_step))
step_item.setFlags(step_item.flags() | Qt.ItemIsEditable)
self.order_table.setItem(row, 4, step_item)
# SMS발송여부 (편집 불가)
sms_status = "전송 완료" if order.domestic_tracking else "미전송"
sms_item = QTableWidgetItem(sms_status)
sms_item.setFlags(sms_item.flags() & ~Qt.ItemIsEditable)
self.order_table.setItem(row, 5, sms_item)
# "지금발송" 버튼 (셀 위젯)
send_btn = QPushButton("지금발송")
send_btn.setToolTip("SMS를 즉시 전송합니다.")
# SMS 미전송인 경우에만 버튼 활성화
send_btn.setEnabled(False if order.domestic_tracking else True)
# 버튼 클릭 시, SMSMessengerWorker의 sendSMS 시그널을 emit하여 기존 스레드의 이벤트 루프에서 SMS 전송을 처리
send_btn.clicked.connect(lambda _, o=order: self.sms_worker.sendSMS.emit(o))
self.order_table.setCellWidget(row, 6, send_btn)
# 최종 업데이트 (편집 불가)
updated_at_str = order.updated_at.strftime("%Y-%m-%d %H:%M:%S") if order.updated_at else ""
update_item = QTableWidgetItem(updated_at_str)
update_item.setFlags(update_item.flags() & ~Qt.ItemIsEditable)
self.order_table.setItem(row, 7, update_item)
self.order_table.blockSignals(False)
@Slot()
def item_changed_slot(self, item):
# 사용자가 셀을 수정한 경우, 수정 내용을 DB에 업데이트합니다.
row = self.order_table.currentRow()
if row < 0:
return
try:
order_id = int(self.order_table.item(row, 0).text())
except Exception:
return
customer_name = self.order_table.item(row, 2).text()
product_name = self.order_table.item(row, 1).text()
customer_phone = self.order_table.item(row, 3).text()
order_step_text = self.order_table.item(row, 4).text()
try:
order_step = int(order_step_text)
except ValueError:
QMessageBox.warning(self, "입력 오류", "진행 단계는 정수 값으로 입력해주세요.")
return
updated_order = self.db_manager.update_order(
order_id,
customer_name=customer_name,
product_name=product_name,
customer_phone=customer_phone,
order_step=order_step
)
QMessageBox.information(self, "저장 확인", f"주문서(ID {order_id})가 수정되었습니다.")
self.logger.log(
f"주문서(ID {order_id}) 수정: 고객명={customer_name}, 상품명={product_name}, 전화번호={customer_phone}, 진행단계={order_step}",
level=logging.INFO
)
def apply_styles(self):
style = """
QMainWindow {
background-color: #f7f7f7;
}
QPushButton {
background-color: #4CAF50;
color: white;
border-radius: 6px;
padding: 8px 16px;
font-size: 14px;
}
QPushButton:hover {
background-color: #45a049;
}
QTextEdit {
background-color: white;
border: 1px solid #ccc;
border-radius: 4px;
font-size: 12px;
}
QTableWidget {
background-color: white;
border: 1px solid #ccc;
}
QMenuBar {
background-color: #333;
color: white;
}
QMenuBar::item {
background-color: #333;
padding: 4px 10px;
}
QMenuBar::item:selected {
background-color: #555;
}
QMenu {
background-color: #444;
color: white;
}
QMenu::item:selected {
background-color: #666;
}
"""
self.setStyleSheet(style)
@Slot(str)
def show_qr_dialog(self, qr_image_path: str):
"""Worker에서 QR 코드 감지 시 호출되어, QR 다이얼로그 표시"""
self.logger.log("MainWindow: QR 다이얼로그 표시", level=logging.INFO)
if self.qr_dialog is None:
self.qr_dialog = QRDialog(qr_image_path, self)
self.qr_dialog.show()
else:
# 이미 다이얼로그가 열려 있다면 업데이트 (필요 시)
self.qr_dialog.qr_label.setPixmap(
QPixmap(qr_image_path).scaled(280, 280, Qt.KeepAspectRatio, Qt.SmoothTransformation)
)
@Slot()
def handle_login_success(self):
"""Worker에서 로그인 성공 시 호출되어, QR 다이얼로그를 닫고 로그인 성공 메시지 표시"""
self.logger.log("MainWindow: 로그인 성공 처리", level=logging.INFO)
# QR 다이얼로그 닫기
if self.qr_dialog:
self.qr_dialog.close()
self.qr_dialog = None
# 로그인 성공 메시지 표시 (3초 후 자동 닫힘)
success_dialog = LoginSuccessDialog(self)
success_dialog.exec()
# (추가: 메시지 전송 페이지로 이동하는 등 후속 처리 가능)
def closeEvent(self, event):
self.logger.log("MainWindow: 종료 전 cleanup 시작", level=logging.INFO)
# cleanup()를 비동기로 실행한 후, 일정 시간 후 강제로 이벤트 루프 종료를 시도합니다.
asyncio.create_task(self.cleanup())
event.accept()
async def cleanup(self):
try:
self.loop.stop()
self.loop.close()
# disconnect()에 5초 timeout 적용
await asyncio.wait_for(self.sms_worker.sms_messenger.disconnect(), timeout=5)
self.logger.log("MainWindow: disconnect 완료", level=logging.INFO)
except asyncio.TimeoutError:
self.logger.error("MainWindow: disconnect 타임아웃 발생", exc_info=True)
except Exception as e:
self.logger.error(f"MainWindow: disconnect 중 에러 발생: {e}", exc_info=True)
finally:
# 미완료 태스크 취소
tasks = [t for t in asyncio.all_tasks() if t is not asyncio.current_task()]
for task in tasks:
task.cancel()
await asyncio.gather(*tasks, return_exceptions=True)
self.logger.log("MainWindow: 모든 태스크 취소 완료", level=logging.INFO)
# 이벤트 루프 종료 요청
QCoreApplication.quit()
import sys
sys.exit(0)
class QRDialog(QDialog):
"""
Google Messages QR 로그인 다이얼로그.
90초 카운트다운을 포함하며, 시간이 지나면 창을 자동으로 닫음.
"""
def __init__(self, qr_image_path, parent=None):
super().__init__(parent)
self.setWindowTitle("Google Messages 로그인")
self.setFixedSize(350, 500) # 창 크기 조정 (라벨 여유 공간 추가)
# 90초 카운트다운 설정
self.countdown_time = 90
layout = QVBoxLayout(self)
# QR 코드 이미지 표시
self.qr_label = QLabel()
self.qr_label.setPixmap(QPixmap(qr_image_path).scaled(300, 300, Qt.KeepAspectRatio, Qt.SmoothTransformation))
layout.addWidget(self.qr_label, alignment=Qt.AlignCenter)
# 로그인 안내 메시지
self.info_label = QLabel("📷 QR 코드를 스캔하여 로그인하세요!")
self.info_label.setAlignment(Qt.AlignCenter)
self.info_label.setFont(QFont("Arial", 12, QFont.Bold))
layout.addWidget(self.info_label)
# ⏳ 카운트다운 라벨 (크고 굵게)
self.countdown_label = QLabel()
self.countdown_label.setAlignment(Qt.AlignCenter)
self.countdown_label.setFont(QFont("Arial", 20, QFont.Bold))
self.countdown_label.setStyleSheet("color: red;") # 빨간색 강조
layout.addWidget(self.countdown_label)
# 닫기 버튼 (사용자가 직접 닫을 수 있도록)
self.ok_button = QPushButton("❌ 닫기")
self.ok_button.setFont(QFont("Arial", 12))
self.ok_button.setStyleSheet("background-color: #d9534f; color: white; padding: 8px;")
self.ok_button.clicked.connect(self.reject)
layout.addWidget(self.ok_button)
self.setLayout(layout)
# 타이머 설정 (1초마다 update_countdown 호출)
self.timer = QTimer(self)
self.timer.timeout.connect(self.update_countdown)
self.timer.start(1000)
self.update_countdown() # 초기에 한 번 업데이트하여 현재 시간 표시
def update_countdown(self):
"""카운트다운을 업데이트하고 UI를 갱신"""
if self.countdown_time > 0:
minutes = self.countdown_time // 60
seconds = self.countdown_time % 60
self.countdown_label.setText(f"⏳ 남은 시간: {minutes}:{seconds:02d}")
self.countdown_time -= 1
QCoreApplication.processEvents() # UI 즉시 업데이트
else:
self.timer.stop()
self.reject() # 시간이 다 되면 창 닫기
class LoginSuccessDialog(QMessageBox):
"""로그인 성공 시 표시되는 메시지 박스 (5초 후 자동 닫힘)"""
def __init__(self, parent=None):
super().__init__(parent)
self.setWindowTitle("로그인 성공")
self.setText("Google Messages 로그인에 성공했습니다.")
self.setIcon(QMessageBox.Information)
self.setStandardButtons(QMessageBox.NoButton) # 버튼 없음 (자동 닫힘)
# 5초 후 자동 닫기
self.timer = QTimer(self)
self.timer.setSingleShot(True)
self.timer.timeout.connect(self.accept)
self.timer.start(2500) # 5초 (5000ms)
class SMSInputDialog(QDialog):
def __init__(self, parent=None):
super().__init__(parent)
self.setWindowTitle("테스트 SMS 입력")
self.setFixedSize(300, 200)
layout = QVBoxLayout(self)
self.recipient_label = QLabel("수신자 전화번호:")
layout.addWidget(self.recipient_label)
self.recipient_edit = QLineEdit()
layout.addWidget(self.recipient_edit)
self.message_label = QLabel("메시지 내용:")
layout.addWidget(self.message_label)
self.message_edit = QTextEdit()
layout.addWidget(self.message_edit)
self.ok_button = QPushButton("전송")
self.ok_button.clicked.connect(self.accept)
layout.addWidget(self.ok_button)