475 lines
20 KiB
Python
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)
|