diff --git a/backend/gui/pos_sales_gui.py b/backend/gui/pos_sales_gui.py index 74e00fd..c3d3372 100644 --- a/backend/gui/pos_sales_gui.py +++ b/backend/gui/pos_sales_gui.py @@ -9,15 +9,19 @@ from datetime import datetime from PyQt5.QtWidgets import ( QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, QPushButton, QLabel, QGroupBox, QTableWidget, QTableWidgetItem, - QDialog, QMessageBox, QDateEdit + QDialog, QMessageBox, QDateEdit, QCheckBox ) -from PyQt5.QtCore import QThread, pyqtSignal, Qt, QDate +from PyQt5.QtCore import QThread, pyqtSignal, Qt, QDate, QTimer from PyQt5.QtGui import QFont # 데이터베이스 연결 (backend/ 폴더를 Python 경로에 추가) sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..')) from db.dbsetup import DatabaseManager +# QR 생성 모듈 import +from utils.qr_token_generator import generate_claim_token, save_token_to_db +from utils.qr_label_printer import print_qr_label + class SalesQueryThread(QThread): """ @@ -36,12 +40,19 @@ class SalesQueryThread(QThread): self.date_str = date_str def run(self): - """스레드 실행 (SALE_MAIN 조회)""" - conn = None + """스레드 실행 (SALE_MAIN 조회 + SQLite 적립 사용자 조회)""" + mssql_conn = None + sqlite_conn = None try: db_manager = DatabaseManager() - conn = db_manager.get_engine('PM_PRES').raw_connection() - cursor = conn.cursor() + + # MSSQL 연결 + mssql_conn = db_manager.get_engine('PM_PRES').raw_connection() + mssql_cursor = mssql_conn.cursor() + + # SQLite 연결 + sqlite_conn = db_manager.get_sqlite_connection() + sqlite_cursor = sqlite_conn.cursor() # 메인 쿼리: SALE_MAIN에서 오늘 판매 내역 조회 query = """ @@ -55,27 +66,43 @@ class SalesQueryThread(QThread): ORDER BY M.InsertTime DESC """ - cursor.execute(query, self.date_str) - rows = cursor.fetchall() + mssql_cursor.execute(query, self.date_str) + rows = mssql_cursor.fetchall() sales_list = [] for row in rows: order_no, insert_time, sale_amount, customer = row # 품목 수 조회 (SALE_SUB) - cursor.execute(""" + mssql_cursor.execute(""" SELECT COUNT(*) FROM SALE_SUB WHERE SL_NO_order = ? """, order_no) - item_count_row = cursor.fetchone() + item_count_row = mssql_cursor.fetchone() item_count = item_count_row[0] if item_count_row else 0 + # SQLite에서 적립 사용자 조회 + sqlite_cursor.execute(""" + SELECT u.nickname, u.phone + FROM claim_tokens ct + LEFT JOIN users u ON ct.claimed_by_user_id = u.id + WHERE ct.transaction_id = ? AND ct.claimed_at IS NOT NULL + """, (order_no,)) + claimed_user = sqlite_cursor.fetchone() + + # 적립 사용자 정보 포맷팅 + if claimed_user and claimed_user['nickname'] and claimed_user['phone']: + claimed_info = f"{claimed_user['nickname']} ({claimed_user['phone']})" + else: + claimed_info = "" + sales_list.append({ 'order_no': order_no, 'time': insert_time.strftime('%H:%M') if insert_time else '--:--', 'amount': float(sale_amount) if sale_amount else 0.0, 'customer': customer, - 'item_count': item_count + 'item_count': item_count, + 'claimed_user': claimed_info }) self.query_complete.emit(sales_list) @@ -83,8 +110,150 @@ class SalesQueryThread(QThread): except Exception as e: self.query_error.emit(str(e)) finally: - if conn: - conn.close() + if mssql_conn: + mssql_conn.close() + if sqlite_conn: + sqlite_conn.close() + + +class QRGeneratorThread(QThread): + """ + QR 토큰 생성 및 라벨 출력 백그라운드 스레드 + GUI 블로킹 방지 + """ + qr_complete = pyqtSignal(bool, str, str) # 성공 여부, 메시지, 이미지 경로 + + def __init__(self, transaction_id, total_amount, transaction_time, preview_mode=False): + """ + Args: + transaction_id (str): POS 거래 ID + total_amount (float): 판매 금액 + transaction_time (datetime): 거래 시간 + preview_mode (bool): 미리보기 모드 + """ + super().__init__() + self.transaction_id = transaction_id + self.total_amount = total_amount + self.transaction_time = transaction_time + self.preview_mode = preview_mode + + def run(self): + """스레드 실행""" + try: + # 1. Claim Token 생성 + token_info = generate_claim_token( + self.transaction_id, + self.total_amount + ) + + # 2. DB 저장 + success, error = save_token_to_db( + self.transaction_id, + token_info['token_hash'], + self.total_amount, + token_info['claimable_points'], + token_info['expires_at'], + token_info['pharmacy_id'] + ) + + if not success: + self.qr_complete.emit(False, error, "") + return + + # 3. QR 라벨 생성 + if self.preview_mode: + # 미리보기 + success, image_path = print_qr_label( + token_info['qr_url'], + self.transaction_id, + self.total_amount, + token_info['claimable_points'], + self.transaction_time, + preview_mode=True + ) + + if success: + self.qr_complete.emit( + True, + f"QR 생성 완료 ({token_info['claimable_points']}P)", + image_path + ) + else: + self.qr_complete.emit(False, "이미지 생성 실패", "") + else: + # 실제 인쇄 + success = print_qr_label( + token_info['qr_url'], + self.transaction_id, + self.total_amount, + token_info['claimable_points'], + self.transaction_time, + preview_mode=False + ) + + if success: + self.qr_complete.emit( + True, + f"QR 출력 완료 ({token_info['claimable_points']}P)", + "" + ) + else: + self.qr_complete.emit(False, "프린터 전송 실패", "") + + except Exception as e: + self.qr_complete.emit(False, f"오류: {str(e)}", "") + + +class QRLabelPreviewDialog(QDialog): + """ + QR 라벨 미리보기 팝업 (barcode_reader_gui.py의 LabelPreviewDialog 참고) + """ + + def __init__(self, image_path, transaction_id, parent=None): + """ + Args: + image_path (str): 미리보기 이미지 파일 경로 + transaction_id (str): 거래 번호 + parent: 부모 위젯 + """ + super().__init__(parent) + self.image_path = image_path + self.transaction_id = transaction_id + self.init_ui() + + def init_ui(self): + """UI 초기화""" + self.setWindowTitle(f'QR 라벨 미리보기 - {self.transaction_id}') + self.setModal(False) # 모달 아님 (계속 작업 가능) + + layout = QVBoxLayout() + + # 안내 라벨 + info_label = QLabel('[미리보기] 실제 인쇄하려면 "미리보기 모드" 체크를 해제하세요.') + info_label.setStyleSheet('color: #2196F3; font-size: 12px; padding: 10px;') + layout.addWidget(info_label) + + # 이미지 표시 + from PyQt5.QtGui import QPixmap + pixmap = QPixmap(self.image_path) + + # 스케일링 (최대 1000px 폭) + if pixmap.width() > 1000: + pixmap = pixmap.scaledToWidth(1000, Qt.SmoothTransformation) + + image_label = QLabel() + image_label.setPixmap(pixmap) + image_label.setAlignment(Qt.AlignCenter) + layout.addWidget(image_label) + + # 닫기 버튼 + close_btn = QPushButton('닫기') + close_btn.setStyleSheet('background-color: #2196F3; color: white; font-weight: bold; padding: 8px 20px;') + close_btn.clicked.connect(self.close) + layout.addWidget(close_btn) + + self.setLayout(layout) + self.adjustSize() class SaleDetailDialog(QDialog): @@ -197,13 +366,14 @@ class POSSalesGUI(QMainWindow): super().__init__() self.db_manager = DatabaseManager() self.sales_thread = None + self.qr_thread = None # QR 생성 스레드 추가 self.sales_data = [] self.init_ui() def init_ui(self): """UI 초기화""" self.setWindowTitle('POS 판매 조회') - self.setGeometry(100, 100, 900, 600) + self.setGeometry(100, 100, 1100, 600) # 중앙 위젯 central_widget = QWidget() @@ -232,13 +402,21 @@ class POSSalesGUI(QMainWindow): self.refresh_btn.clicked.connect(self.refresh_sales) settings_layout.addWidget(self.refresh_btn) - # QR 생성 버튼 (Phase 2 준비 - 현재 비활성화) + # QR 생성 버튼 (활성화) self.qr_btn = QPushButton('QR 생성') - self.qr_btn.setEnabled(False) - self.qr_btn.setStyleSheet('background-color: #9E9E9E; color: white; padding: 8px; font-weight: bold;') - self.qr_btn.setToolTip('후향적 적립 QR (추후 개발)') + self.qr_btn.setEnabled(True) # 활성화! + self.qr_btn.setStyleSheet('background-color: #FF9800; color: white; padding: 8px; font-weight: bold;') + self.qr_btn.setToolTip('선택된 거래의 QR 적립 라벨 생성') + self.qr_btn.clicked.connect(self.generate_qr_label) # 이벤트 연결 settings_layout.addWidget(self.qr_btn) + # 미리보기 모드 체크박스 추가 + self.preview_checkbox = QCheckBox('미리보기 모드') + self.preview_checkbox.setChecked(True) # 기본값: 미리보기 + self.preview_checkbox.setStyleSheet('font-size: 12px; color: #4CAF50;') + self.preview_checkbox.setToolTip('체크: PNG 미리보기, 해제: 프린터 직접 출력') + settings_layout.addWidget(self.preview_checkbox) + settings_layout.addStretch() main_layout.addWidget(settings_group) @@ -249,15 +427,16 @@ class POSSalesGUI(QMainWindow): sales_group.setLayout(sales_layout) self.sales_table = QTableWidget() - self.sales_table.setColumnCount(5) + self.sales_table.setColumnCount(6) self.sales_table.setHorizontalHeaderLabels([ - '주문번호', '시간', '금액', '고객명', '품목수' + '주문번호', '시간', '금액', '고객명', '품목수', '적립 사용자' ]) - self.sales_table.setColumnWidth(0, 180) - self.sales_table.setColumnWidth(1, 80) - self.sales_table.setColumnWidth(2, 120) - self.sales_table.setColumnWidth(3, 120) - self.sales_table.setColumnWidth(4, 80) + self.sales_table.setColumnWidth(0, 160) + self.sales_table.setColumnWidth(1, 70) + self.sales_table.setColumnWidth(2, 110) + self.sales_table.setColumnWidth(3, 100) + self.sales_table.setColumnWidth(4, 70) + self.sales_table.setColumnWidth(5, 180) self.sales_table.setSelectionBehavior(QTableWidget.SelectRows) self.sales_table.doubleClicked.connect(self.show_sale_detail) @@ -283,6 +462,11 @@ class POSSalesGUI(QMainWindow): # 초기 조회 실행 self.refresh_sales() + # 자동 새로고침 타이머 (30초마다) + self.refresh_timer = QTimer() + self.refresh_timer.timeout.connect(self.refresh_sales) + self.refresh_timer.start(30000) # 30초 = 30000ms + def on_date_changed(self, date): """날짜 변경 시 유효성 검사""" today = QDate.currentDate() @@ -339,7 +523,7 @@ class POSSalesGUI(QMainWindow): amount_item.setTextAlignment(Qt.AlignRight | Qt.AlignVCenter) self.sales_table.setItem(row, 2, amount_item) - # 고객명 + # 고객명 (MSSQL) self.sales_table.setItem(row, 3, QTableWidgetItem(sale['customer'])) # 품목수 (중앙 정렬) @@ -347,6 +531,16 @@ class POSSalesGUI(QMainWindow): count_item.setTextAlignment(Qt.AlignCenter) self.sales_table.setItem(row, 4, count_item) + # 적립 사용자 (SQLite) + claimed_item = QTableWidgetItem(sale['claimed_user']) + if sale['claimed_user']: + from PyQt5.QtGui import QColor, QFont + claimed_item.setForeground(QColor('#4CAF50')) + font = QFont() + font.setBold(True) + claimed_item.setFont(font) + self.sales_table.setItem(row, 5, claimed_item) + def on_query_error(self, error_msg): """DB 조회 에러 처리""" QMessageBox.critical(self, '오류', f'조회 실패:\n{error_msg}') @@ -364,10 +558,89 @@ class POSSalesGUI(QMainWindow): detail_dialog = SaleDetailDialog(order_no, self) detail_dialog.exec_() + def generate_qr_label(self): + """선택된 판매 건에 대해 QR 라벨 생성""" + # 선택된 행 확인 + current_row = self.sales_table.currentRow() + if current_row < 0: + QMessageBox.warning(self, '경고', '거래를 선택해주세요.') + return + + # 거래 정보 가져오기 + order_no = self.sales_table.item(current_row, 0).text() + amount_text = self.sales_table.item(current_row, 2).text() + + # 금액 파싱 (예: "50,000원" → 50000.0) + amount = float(amount_text.replace(',', '').replace('원', '')) + + # sales_data에서 거래 시간 찾기 + sale = next((s for s in self.sales_data if s['order_no'] == order_no), None) + if not sale: + QMessageBox.warning(self, '오류', '거래 정보를 찾을 수 없습니다.') + return + + # 거래 시간 파싱 + date_str = self.date_edit.date().toString('yyyy-MM-dd') + time_str = sale['time'] + transaction_time = datetime.strptime(f"{date_str} {time_str}", '%Y-%m-%d %H:%M') + + # 중복 방지 확인 (이미 QR 생성된 거래인지) + if self.qr_thread and self.qr_thread.isRunning(): + QMessageBox.warning(self, '경고', 'QR 생성 중입니다. 잠시만 기다려주세요.') + return + + # 미리보기 모드 확인 + preview_mode = self.preview_checkbox.isChecked() + + # QR 생성 스레드 시작 + self.qr_thread = QRGeneratorThread( + order_no, + amount, + transaction_time, + preview_mode + ) + self.qr_thread.qr_complete.connect(self.on_qr_generated) + self.qr_thread.start() + + # 상태 표시 + self.status_label.setText(f'QR 생성 중... ({order_no})') + self.status_label.setStyleSheet('color: orange; font-size: 12px; padding: 5px;') + self.qr_btn.setEnabled(False) + + def on_qr_generated(self, success, message, image_path): + """QR 생성 완료 시그널 핸들러""" + # 버튼 재활성화 + self.qr_btn.setEnabled(True) + + if success: + # 성공 + self.status_label.setText(f'✓ {message}') + self.status_label.setStyleSheet('color: green; font-size: 12px; padding: 5px;') + + # 미리보기 모드면 Dialog 표시 + if image_path: + order_no = self.sales_table.item(self.sales_table.currentRow(), 0).text() + preview_dialog = QRLabelPreviewDialog(image_path, order_no, self) + preview_dialog.show() + else: + # 실제 인쇄 완료 + QMessageBox.information(self, '완료', f'{message}\n프린터: 192.168.0.168') + else: + # 실패 + self.status_label.setText('QR 생성 실패') + self.status_label.setStyleSheet('color: red; font-size: 12px; padding: 5px;') + QMessageBox.critical(self, '오류', f'QR 생성 실패:\n{message}') + def closeEvent(self, event): """종료 시 정리""" + # 자동 새로고침 타이머 중지 + if hasattr(self, 'refresh_timer'): + self.refresh_timer.stop() + if self.sales_thread and self.sales_thread.isRunning(): self.sales_thread.wait() + if self.qr_thread and self.qr_thread.isRunning(): # QR 스레드 추가 + self.qr_thread.wait() self.db_manager.close_all() event.accept()