- test_integration.py: QR 토큰 생성 및 라벨 테스트 - samples/barcode_print.py: Brother QL 프린터 예제 - samples/barcode_reader_gui.py: 바코드 리더 GUI 참고 코드 Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
693 lines
26 KiB
Python
693 lines
26 KiB
Python
"""
|
|
허니웰 바코드 리더기 GUI 프로그램 (PyQt5)
|
|
COM3 포트에서 바코드를 실시간으로 읽어 화면에 표시
|
|
MSSQL DB에서 약품 정보 조회 기능 포함
|
|
"""
|
|
|
|
import sys
|
|
import serial
|
|
import serial.tools.list_ports
|
|
from datetime import datetime
|
|
from PyQt5.QtWidgets import (
|
|
QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout,
|
|
QPushButton, QTextEdit, QComboBox, QLabel, QGroupBox, QSpinBox,
|
|
QCheckBox, QDialog
|
|
)
|
|
from PyQt5.QtCore import QThread, pyqtSignal, Qt
|
|
from PyQt5.QtGui import QFont, QTextCursor, QPixmap
|
|
from sqlalchemy import text
|
|
|
|
# MSSQL 데이터베이스 연결
|
|
sys.path.insert(0, '.')
|
|
from dbsetup import DatabaseManager
|
|
|
|
# 바코드 라벨 출력
|
|
from barcode_print import print_barcode_label
|
|
|
|
|
|
def parse_gs1_barcode(barcode):
|
|
"""
|
|
GS1-128 바코드 파싱
|
|
|
|
Args:
|
|
barcode: 원본 바코드 문자열
|
|
|
|
Returns:
|
|
list: 파싱된 바코드 후보 리스트 (우선순위 순)
|
|
"""
|
|
candidates = [barcode] # 원본 바코드를 첫 번째 후보로
|
|
|
|
# GS1-128: 01로 시작하는 경우 (GTIN)
|
|
if barcode.startswith('01') and len(barcode) >= 16:
|
|
# 01 + 14자리 GTIN
|
|
gtin14 = barcode[2:16]
|
|
candidates.append(gtin14)
|
|
|
|
# GTIN-14를 GTIN-13으로 변환 (앞자리가 0인 경우)
|
|
if gtin14.startswith('0'):
|
|
gtin13 = gtin14[1:]
|
|
candidates.append(gtin13)
|
|
|
|
# GS1-128: 01로 시작하지만 13자리인 경우
|
|
elif barcode.startswith('01') and len(barcode) == 15:
|
|
gtin13 = barcode[2:15]
|
|
candidates.append(gtin13)
|
|
|
|
return candidates
|
|
|
|
|
|
def search_drug_by_barcode(barcode):
|
|
"""
|
|
바코드로 약품 정보 조회 (MSSQL PM_DRUG.CD_GOODS)
|
|
GS1-128 바코드 자동 파싱 지원
|
|
|
|
Args:
|
|
barcode: 바코드 번호
|
|
|
|
Returns:
|
|
tuple: (약품 정보 dict 또는 None, 파싱 정보 dict)
|
|
"""
|
|
try:
|
|
db_manager = DatabaseManager()
|
|
engine = db_manager.get_engine('PM_DRUG')
|
|
|
|
query = text('''
|
|
SELECT TOP 1
|
|
BARCODE,
|
|
GoodsName,
|
|
DrugCode,
|
|
SplName,
|
|
Price,
|
|
Saleprice,
|
|
SUNG_CODE,
|
|
IsUSE
|
|
FROM CD_GOODS
|
|
WHERE BARCODE = :barcode
|
|
AND (GoodsName NOT LIKE N'%(판매중지)%' AND GoodsName NOT LIKE N'%(판매중단)%')
|
|
ORDER BY
|
|
CASE WHEN IsUSE = '1' THEN 0 ELSE 1 END, -- 1. 사용중인 제품 우선
|
|
CASE WHEN Price > 0 THEN 0 ELSE 1 END, -- 2. 가격 정보 있는 제품 우선
|
|
CASE WHEN SplName IS NOT NULL AND SplName != '' THEN 0 ELSE 1 END, -- 3. 제조사 정보 있는 제품 우선
|
|
DrugCode DESC -- 4. 약품코드 내림차순
|
|
''')
|
|
|
|
# GS1 바코드 파싱
|
|
candidates = parse_gs1_barcode(barcode)
|
|
parse_info = {
|
|
'original': barcode,
|
|
'candidates': candidates,
|
|
'matched_barcode': None,
|
|
'is_gs1': len(candidates) > 1
|
|
}
|
|
|
|
with engine.connect() as conn:
|
|
# 여러 후보 바코드로 순차 검색
|
|
for candidate in candidates:
|
|
result = conn.execute(query, {"barcode": candidate})
|
|
row = result.fetchone()
|
|
|
|
if row:
|
|
parse_info['matched_barcode'] = candidate
|
|
drug_info = {
|
|
'barcode': row.BARCODE,
|
|
'goods_name': row.GoodsName,
|
|
'drug_code': row.DrugCode,
|
|
'manufacturer': row.SplName,
|
|
'price': float(row.Price) if row.Price else 0,
|
|
'sale_price': float(row.Saleprice) if row.Saleprice else 0,
|
|
'sung_code': row.SUNG_CODE if row.SUNG_CODE else ''
|
|
}
|
|
return drug_info, parse_info
|
|
|
|
return None, parse_info
|
|
|
|
except Exception as e:
|
|
print(f'[오류] 약품 조회 실패: {e}')
|
|
return None, {'original': barcode, 'error': str(e)}
|
|
|
|
|
|
class DrugSearchThread(QThread):
|
|
"""약품 정보 조회 전용 백그라운드 스레드"""
|
|
|
|
# 시그널: (바코드, 타임스탬프, 원본 데이터, 약품 정보, 파싱 정보)
|
|
search_complete = pyqtSignal(str, str, bytes, object, object)
|
|
|
|
def __init__(self, barcode, timestamp, raw_data):
|
|
super().__init__()
|
|
self.barcode = barcode
|
|
self.timestamp = timestamp
|
|
self.raw_data = raw_data
|
|
|
|
def run(self):
|
|
"""백그라운드에서 DB 조회"""
|
|
drug_info, parse_info = search_drug_by_barcode(self.barcode)
|
|
self.search_complete.emit(self.barcode, self.timestamp, self.raw_data, drug_info, parse_info)
|
|
|
|
|
|
class LabelGeneratorThread(QThread):
|
|
"""라벨 이미지 생성 전용 백그라운드 스레드"""
|
|
|
|
# 시그널: (성공 여부, 이미지 경로, 약품명, 에러 메시지)
|
|
image_ready = pyqtSignal(bool, str, str, str)
|
|
|
|
def __init__(self, goods_name, sale_price, preview_mode=False):
|
|
super().__init__()
|
|
self.goods_name = goods_name
|
|
self.sale_price = sale_price
|
|
self.preview_mode = preview_mode
|
|
|
|
def run(self):
|
|
"""백그라운드에서 이미지 생성"""
|
|
try:
|
|
if self.preview_mode:
|
|
# 미리보기 모드
|
|
success, image_path = print_barcode_label(
|
|
self.goods_name,
|
|
self.sale_price,
|
|
preview_mode=True
|
|
)
|
|
if success:
|
|
self.image_ready.emit(True, image_path, self.goods_name, "")
|
|
else:
|
|
self.image_ready.emit(False, "", self.goods_name, "이미지 생성 실패")
|
|
else:
|
|
# 실제 인쇄 모드
|
|
success = print_barcode_label(
|
|
self.goods_name,
|
|
self.sale_price,
|
|
preview_mode=False
|
|
)
|
|
if success:
|
|
self.image_ready.emit(True, "", self.goods_name, "")
|
|
else:
|
|
self.image_ready.emit(False, "", self.goods_name, "라벨 출력 실패")
|
|
except Exception as e:
|
|
self.image_ready.emit(False, "", self.goods_name, str(e))
|
|
|
|
|
|
class LabelPreviewDialog(QDialog):
|
|
"""라벨 미리보기 팝업 창"""
|
|
|
|
def __init__(self, image_path, goods_name, parent=None):
|
|
"""
|
|
Args:
|
|
image_path: 미리보기 이미지 파일 경로
|
|
goods_name: 약품명
|
|
parent: 부모 위젯
|
|
"""
|
|
super().__init__(parent)
|
|
self.image_path = image_path
|
|
self.goods_name = goods_name
|
|
self.init_ui()
|
|
|
|
def init_ui(self):
|
|
"""UI 초기화"""
|
|
self.setWindowTitle(f'라벨 미리보기 - {self.goods_name}')
|
|
self.setModal(False) # 모달 아님 (계속 스캔 가능)
|
|
|
|
# 레이아웃
|
|
layout = QVBoxLayout()
|
|
|
|
# 상단 안내 라벨
|
|
info_label = QLabel('[미리보기] 실제 인쇄하려면 "미리보기 모드" 체크를 해제하세요.')
|
|
info_label.setStyleSheet('color: #2196F3; font-size: 12px; padding: 10px;')
|
|
layout.addWidget(info_label)
|
|
|
|
# 이미지 표시 (QLabel + 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)
|
|
|
|
# 버튼 레이아웃
|
|
button_layout = QHBoxLayout()
|
|
|
|
# 닫기 버튼
|
|
close_btn = QPushButton('닫기')
|
|
close_btn.setStyleSheet('background-color: #2196F3; color: white; font-weight: bold; padding: 8px 20px;')
|
|
close_btn.clicked.connect(self.close)
|
|
button_layout.addWidget(close_btn)
|
|
|
|
layout.addLayout(button_layout)
|
|
|
|
self.setLayout(layout)
|
|
|
|
# 창 크기 자동 조정
|
|
self.adjustSize()
|
|
|
|
|
|
class BarcodeReaderThread(QThread):
|
|
"""바코드 읽기 스레드 (DB 조회 없이 바코드만 읽음)"""
|
|
barcode_received = pyqtSignal(str, str, bytes) # 바코드, 시간, 원본 (DB 조회 제외!)
|
|
connection_status = pyqtSignal(bool, str) # 연결 상태, 메시지
|
|
raw_data_received = pyqtSignal(str) # 시리얼 포트 RAW 데이터 (디버깅용)
|
|
|
|
def __init__(self, port='COM3', baudrate=115200):
|
|
super().__init__()
|
|
self.port = port
|
|
self.baudrate = baudrate
|
|
self.running = False
|
|
self.serial_connection = None
|
|
|
|
def run(self):
|
|
"""스레드 실행"""
|
|
self.running = True
|
|
|
|
try:
|
|
# 시리얼 포트 열기
|
|
self.serial_connection = serial.Serial(
|
|
port=self.port,
|
|
baudrate=self.baudrate,
|
|
bytesize=serial.EIGHTBITS,
|
|
parity=serial.PARITY_NONE,
|
|
stopbits=serial.STOPBITS_ONE,
|
|
timeout=1
|
|
)
|
|
|
|
self.connection_status.emit(True, f'{self.port} 연결 성공! (속도: {self.baudrate} bps)')
|
|
|
|
# 바코드 읽기 루프
|
|
while self.running:
|
|
if self.serial_connection.in_waiting > 0:
|
|
buffer_size = self.serial_connection.in_waiting
|
|
timestamp_ms = datetime.now().strftime('%H:%M:%S.%f')[:-3]
|
|
|
|
# 즉시 GUI에 표시
|
|
self.raw_data_received.emit(f'[{timestamp_ms}] 버퍼: {buffer_size} bytes')
|
|
|
|
# 버퍼의 모든 데이터를 한 번에 읽기 (연속 스캔 대응)
|
|
all_data = self.serial_connection.read(buffer_size)
|
|
|
|
# 즉시 GUI에 표시
|
|
self.raw_data_received.emit(f' → 읽음: {all_data.hex()} ({len(all_data)} bytes)')
|
|
|
|
# 디코딩
|
|
try:
|
|
all_text = all_data.decode('utf-8')
|
|
except UnicodeDecodeError:
|
|
all_text = all_data.decode('ascii', errors='ignore')
|
|
|
|
# 개행문자로 분리 (여러 바코드가 함께 들어온 경우)
|
|
lines = all_text.strip().split('\n')
|
|
self.raw_data_received.emit(f' → 분리된 라인 수: {len(lines)}')
|
|
|
|
for line in lines:
|
|
barcode_str = line.strip()
|
|
|
|
if not barcode_str:
|
|
continue
|
|
|
|
# 즉시 GUI에 표시
|
|
self.raw_data_received.emit(f' → 처리: "{barcode_str}" (길이: {len(barcode_str)})')
|
|
|
|
# 바코드 길이 검증 (13자리 EAN-13, 16자리 GS1-128만 허용)
|
|
valid_lengths = [13, 15, 16] # EAN-13, GS1-128 (01+13), GS1-128 (01+14)
|
|
|
|
if len(barcode_str) not in valid_lengths:
|
|
# 비정상 길이: 무시
|
|
self.raw_data_received.emit(f' → [무시] 비정상 길이 {len(barcode_str)}')
|
|
continue
|
|
|
|
timestamp = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
|
|
|
|
# 바코드 데이터만 메인 스레드로 전달
|
|
self.raw_data_received.emit(f' → [OK] 시그널 전송!')
|
|
self.barcode_received.emit(barcode_str, timestamp, barcode_str.encode('utf-8'))
|
|
|
|
# 처리 완료 후 버퍼 확인
|
|
remaining = self.serial_connection.in_waiting
|
|
if remaining > 0:
|
|
self.raw_data_received.emit(f' → [주의] 처리 완료 후 버퍼에 {remaining} bytes 남음 (다음 루프에서 처리)')
|
|
|
|
except serial.SerialException as e:
|
|
self.connection_status.emit(False, f'포트 연결 실패: {str(e)}')
|
|
|
|
except Exception as e:
|
|
self.connection_status.emit(False, f'오류 발생: {str(e)}')
|
|
|
|
finally:
|
|
if self.serial_connection and self.serial_connection.is_open:
|
|
self.serial_connection.close()
|
|
|
|
def stop(self):
|
|
"""스레드 중지"""
|
|
self.running = False
|
|
if self.serial_connection and self.serial_connection.is_open:
|
|
self.serial_connection.close()
|
|
|
|
|
|
class BarcodeReaderGUI(QMainWindow):
|
|
"""바코드 리더 GUI 메인 윈도우"""
|
|
|
|
def __init__(self):
|
|
super().__init__()
|
|
self.reader_thread = None
|
|
self.scan_count = 0
|
|
self.search_threads = [] # 약품 조회 스레드 목록
|
|
self.generator_threads = [] # 라벨 생성 스레드 목록
|
|
self.init_ui()
|
|
|
|
def init_ui(self):
|
|
"""UI 초기화"""
|
|
self.setWindowTitle('허니웰 바코드 리더 - COM 포트')
|
|
self.setGeometry(100, 100, 900, 700)
|
|
|
|
# 중앙 위젯
|
|
central_widget = QWidget()
|
|
self.setCentralWidget(central_widget)
|
|
|
|
# 메인 레이아웃
|
|
main_layout = QVBoxLayout()
|
|
central_widget.setLayout(main_layout)
|
|
|
|
# === 1. 설정 그룹 ===
|
|
settings_group = QGroupBox('연결 설정')
|
|
settings_layout = QHBoxLayout()
|
|
settings_group.setLayout(settings_layout)
|
|
|
|
# COM 포트 선택
|
|
settings_layout.addWidget(QLabel('COM 포트:'))
|
|
self.port_combo = QComboBox()
|
|
self.refresh_ports()
|
|
settings_layout.addWidget(self.port_combo)
|
|
|
|
# 새로고침 버튼
|
|
refresh_btn = QPushButton('새로고침')
|
|
refresh_btn.clicked.connect(self.refresh_ports)
|
|
settings_layout.addWidget(refresh_btn)
|
|
|
|
# 통신 속도
|
|
settings_layout.addWidget(QLabel('속도 (bps):'))
|
|
self.baudrate_spin = QSpinBox()
|
|
self.baudrate_spin.setMinimum(9600)
|
|
self.baudrate_spin.setMaximum(921600)
|
|
self.baudrate_spin.setValue(115200)
|
|
self.baudrate_spin.setSingleStep(9600)
|
|
settings_layout.addWidget(self.baudrate_spin)
|
|
|
|
# 수직 구분선
|
|
settings_layout.addWidget(QLabel('|'))
|
|
|
|
# 미리보기 모드 토글
|
|
self.preview_mode_checkbox = QCheckBox('미리보기 모드 (인쇄 안 함)')
|
|
self.preview_mode_checkbox.setChecked(True) # 기본값: 미리보기 (종이 절약!)
|
|
self.preview_mode_checkbox.setStyleSheet('font-size: 14px; color: #4CAF50; font-weight: bold;')
|
|
settings_layout.addWidget(self.preview_mode_checkbox)
|
|
|
|
settings_layout.addStretch()
|
|
|
|
main_layout.addWidget(settings_group)
|
|
|
|
# === 2. 제어 버튼 ===
|
|
control_layout = QHBoxLayout()
|
|
|
|
self.start_btn = QPushButton('시작')
|
|
self.start_btn.setStyleSheet('background-color: #4CAF50; color: white; font-weight: bold; padding: 10px;')
|
|
self.start_btn.clicked.connect(self.start_reading)
|
|
control_layout.addWidget(self.start_btn)
|
|
|
|
self.stop_btn = QPushButton('중지')
|
|
self.stop_btn.setStyleSheet('background-color: #f44336; color: white; font-weight: bold; padding: 10px;')
|
|
self.stop_btn.setEnabled(False)
|
|
self.stop_btn.clicked.connect(self.stop_reading)
|
|
control_layout.addWidget(self.stop_btn)
|
|
|
|
self.clear_btn = QPushButton('화면 지우기')
|
|
self.clear_btn.setStyleSheet('background-color: #2196F3; color: white; font-weight: bold; padding: 10px;')
|
|
self.clear_btn.clicked.connect(self.clear_output)
|
|
control_layout.addWidget(self.clear_btn)
|
|
|
|
main_layout.addLayout(control_layout)
|
|
|
|
# === 3. 상태 표시 ===
|
|
status_group = QGroupBox('상태')
|
|
status_layout = QVBoxLayout()
|
|
status_group.setLayout(status_layout)
|
|
|
|
self.status_label = QLabel('대기 중...')
|
|
self.status_label.setStyleSheet('color: gray; font-size: 14px; padding: 5px;')
|
|
status_layout.addWidget(self.status_label)
|
|
|
|
self.scan_count_label = QLabel('스캔 횟수: 0')
|
|
self.scan_count_label.setStyleSheet('color: blue; font-size: 14px; font-weight: bold; padding: 5px;')
|
|
status_layout.addWidget(self.scan_count_label)
|
|
|
|
main_layout.addWidget(status_group)
|
|
|
|
# === 4. 바코드 출력 영역 ===
|
|
output_group = QGroupBox('바코드 스캔 결과')
|
|
output_layout = QVBoxLayout()
|
|
output_group.setLayout(output_layout)
|
|
|
|
self.output_text = QTextEdit()
|
|
self.output_text.setReadOnly(True)
|
|
self.output_text.setFont(QFont('Consolas', 10))
|
|
self.output_text.setStyleSheet('background-color: #f5f5f5;')
|
|
output_layout.addWidget(self.output_text)
|
|
|
|
main_layout.addWidget(output_group)
|
|
|
|
def refresh_ports(self):
|
|
"""사용 가능한 COM 포트 새로고침"""
|
|
self.port_combo.clear()
|
|
ports = serial.tools.list_ports.comports()
|
|
|
|
for port in ports:
|
|
self.port_combo.addItem(f'{port.device} - {port.description}', port.device)
|
|
|
|
# COM3이 있으면 선택
|
|
for i in range(self.port_combo.count()):
|
|
if 'COM3' in self.port_combo.itemData(i):
|
|
self.port_combo.setCurrentIndex(i)
|
|
break
|
|
|
|
def start_reading(self):
|
|
"""바코드 읽기 시작"""
|
|
if self.reader_thread and self.reader_thread.isRunning():
|
|
return
|
|
|
|
# 선택된 포트와 속도 가져오기
|
|
port = self.port_combo.currentData()
|
|
if not port:
|
|
self.append_output('[오류] COM 포트를 선택해주세요.')
|
|
return
|
|
|
|
baudrate = self.baudrate_spin.value()
|
|
|
|
# 스레드 시작
|
|
self.reader_thread = BarcodeReaderThread(port, baudrate)
|
|
self.reader_thread.barcode_received.connect(self.on_barcode_received)
|
|
self.reader_thread.connection_status.connect(self.on_connection_status)
|
|
self.reader_thread.raw_data_received.connect(self.on_raw_data) # RAW 데이터 표시
|
|
self.reader_thread.start()
|
|
|
|
# UI 업데이트
|
|
self.start_btn.setEnabled(False)
|
|
self.stop_btn.setEnabled(True)
|
|
self.port_combo.setEnabled(False)
|
|
self.baudrate_spin.setEnabled(False)
|
|
|
|
self.status_label.setText(f'연결 시도 중... ({port}, {baudrate} bps)')
|
|
self.status_label.setStyleSheet('color: orange; font-size: 14px; padding: 5px;')
|
|
|
|
def stop_reading(self):
|
|
"""바코드 읽기 중지"""
|
|
if self.reader_thread:
|
|
self.reader_thread.stop()
|
|
self.reader_thread.wait()
|
|
|
|
# UI 업데이트
|
|
self.start_btn.setEnabled(True)
|
|
self.stop_btn.setEnabled(False)
|
|
self.port_combo.setEnabled(True)
|
|
self.baudrate_spin.setEnabled(True)
|
|
|
|
self.status_label.setText('중지됨')
|
|
self.status_label.setStyleSheet('color: gray; font-size: 14px; padding: 5px;')
|
|
|
|
self.append_output('[시스템] 바코드 리더를 중지했습니다.\n')
|
|
|
|
def on_connection_status(self, success, message):
|
|
"""연결 상태 업데이트"""
|
|
if success:
|
|
self.status_label.setText(f'연결됨: {message}')
|
|
self.status_label.setStyleSheet('color: green; font-size: 14px; font-weight: bold; padding: 5px;')
|
|
self.append_output(f'[시스템] {message}\n')
|
|
self.append_output('[대기] 바코드를 스캔해주세요...\n')
|
|
else:
|
|
self.status_label.setText(f'오류: {message}')
|
|
self.status_label.setStyleSheet('color: red; font-size: 14px; font-weight: bold; padding: 5px;')
|
|
self.append_output(f'[오류] {message}\n')
|
|
self.stop_reading()
|
|
|
|
def on_raw_data(self, log_message):
|
|
"""시리얼 포트 RAW 데이터 즉시 표시 (디버깅용)"""
|
|
self.append_output(log_message + '\n')
|
|
|
|
def on_barcode_received(self, barcode, timestamp, raw_data):
|
|
"""바코드 수신 처리 (DB 조회는 백그라운드 스레드로)"""
|
|
self.scan_count += 1
|
|
self.scan_count_label.setText(f'스캔 횟수: {self.scan_count}')
|
|
|
|
# 즉시 로그 출력 (DB 조회 전)
|
|
output = f'{"=" * 80}\n'
|
|
output += f'[스캔 #{self.scan_count}] {timestamp}\n'
|
|
output += f'바코드: {barcode}\n'
|
|
output += f'길이: {len(barcode)}자\n'
|
|
output += f'[조회 중...] 약품 정보 검색 중\n'
|
|
self.append_output(output)
|
|
|
|
# 백그라운드 스레드로 DB 조회 작업 위임
|
|
search_thread = DrugSearchThread(barcode, timestamp, raw_data)
|
|
search_thread.search_complete.connect(self.on_search_complete)
|
|
search_thread.start()
|
|
self.search_threads.append(search_thread)
|
|
|
|
def on_search_complete(self, barcode, timestamp, raw_data, drug_info, parse_info):
|
|
"""약품 조회 완료 시그널 핸들러 (백그라운드 스레드에서 호출)"""
|
|
# 출력
|
|
output = ''
|
|
|
|
# GS1 파싱 정보 출력
|
|
if parse_info and parse_info.get('is_gs1'):
|
|
output += f'[GS1-128 바코드 감지]\n'
|
|
output += f' 원본 바코드: {parse_info["original"]}\n'
|
|
if parse_info.get('matched_barcode'):
|
|
output += f' 매칭된 바코드: {parse_info["matched_barcode"]}\n'
|
|
if len(parse_info.get('candidates', [])) > 1:
|
|
output += f' 검색 시도: {", ".join(parse_info["candidates"])}\n'
|
|
output += '\n'
|
|
|
|
# 약품 정보 출력
|
|
if drug_info:
|
|
output += f'[약품 정보]\n'
|
|
output += f' 약품명: {drug_info["goods_name"]}\n'
|
|
output += f' 약품코드: {drug_info["drug_code"]}\n'
|
|
output += f' 제조사: {drug_info["manufacturer"]}\n'
|
|
output += f' 매입가: {drug_info["price"]:,.0f}원\n'
|
|
output += f' 판매가: {drug_info["sale_price"]:,.0f}원\n'
|
|
if drug_info["sung_code"]:
|
|
output += f' 성분코드: {drug_info["sung_code"]}\n'
|
|
|
|
# 라벨 출력 또는 미리보기 (백그라운드 스레드)
|
|
try:
|
|
is_preview = self.preview_mode_checkbox.isChecked()
|
|
|
|
# 백그라운드 스레드로 이미지 생성 작업 위임
|
|
generator_thread = LabelGeneratorThread(
|
|
drug_info["goods_name"],
|
|
drug_info["sale_price"],
|
|
preview_mode=is_preview
|
|
)
|
|
|
|
# 완료 시그널 연결
|
|
generator_thread.image_ready.connect(self.on_label_generated)
|
|
|
|
# 스레드 시작 및 목록에 추가
|
|
generator_thread.start()
|
|
self.generator_threads.append(generator_thread)
|
|
|
|
# 로그 출력
|
|
if is_preview:
|
|
output += f'\n[미리보기] 이미지 생성 중...\n'
|
|
else:
|
|
output += f'\n[출력] 라벨 출력 중...\n'
|
|
|
|
except Exception as e:
|
|
output += f'\n[출력 오류] {str(e)}\n'
|
|
else:
|
|
output += f'[약품 정보] 데이터베이스에서 찾을 수 없습니다.\n'
|
|
|
|
output += f'\n원본(HEX): {raw_data.hex()}\n'
|
|
output += f'{"-" * 80}\n\n'
|
|
|
|
self.append_output(output)
|
|
|
|
# 완료된 스레드 정리
|
|
sender_thread = self.sender()
|
|
if sender_thread in self.search_threads:
|
|
self.search_threads.remove(sender_thread)
|
|
|
|
def on_label_generated(self, success, image_path, goods_name, error_msg):
|
|
"""
|
|
라벨 생성 완료 시그널 핸들러 (백그라운드 스레드에서 호출)
|
|
|
|
Args:
|
|
success: 성공 여부
|
|
image_path: 미리보기 이미지 경로 (미리보기 모드일 때만)
|
|
goods_name: 약품명
|
|
error_msg: 에러 메시지 (실패 시)
|
|
"""
|
|
if success:
|
|
if image_path:
|
|
# 미리보기 모드: Dialog 표시
|
|
self.append_output(f'[미리보기 완료] {goods_name}\n')
|
|
preview_dialog = LabelPreviewDialog(image_path, goods_name, self)
|
|
preview_dialog.show()
|
|
else:
|
|
# 실제 인쇄 모드: 성공 로그
|
|
self.append_output(f'[출력 완료] {goods_name} (192.168.0.168)\n')
|
|
else:
|
|
# 실패
|
|
self.append_output(f'[오류] {goods_name}: {error_msg}\n')
|
|
|
|
# 완료된 스레드 정리
|
|
sender_thread = self.sender()
|
|
if sender_thread in self.generator_threads:
|
|
self.generator_threads.remove(sender_thread)
|
|
|
|
def append_output(self, text):
|
|
"""출력 영역에 텍스트 추가"""
|
|
self.output_text.append(text)
|
|
# 스크롤을 맨 아래로
|
|
self.output_text.moveCursor(QTextCursor.End)
|
|
|
|
def clear_output(self):
|
|
"""출력 화면 지우기"""
|
|
self.output_text.clear()
|
|
self.scan_count = 0
|
|
self.scan_count_label.setText('스캔 횟수: 0')
|
|
|
|
def closeEvent(self, event):
|
|
"""프로그램 종료 시 스레드 정리"""
|
|
# 바코드 리더 스레드 종료
|
|
if self.reader_thread:
|
|
self.reader_thread.stop()
|
|
self.reader_thread.wait()
|
|
|
|
# 활성 약품 조회 스레드 종료
|
|
for thread in self.search_threads:
|
|
if thread.isRunning():
|
|
thread.wait()
|
|
|
|
# 활성 라벨 생성 스레드 종료
|
|
for thread in self.generator_threads:
|
|
if thread.isRunning():
|
|
thread.wait()
|
|
|
|
event.accept()
|
|
|
|
|
|
def main():
|
|
"""메인 함수"""
|
|
app = QApplication(sys.argv)
|
|
|
|
# 애플리케이션 스타일
|
|
app.setStyle('Fusion')
|
|
|
|
# 메인 윈도우 생성 및 표시
|
|
window = BarcodeReaderGUI()
|
|
window.show()
|
|
|
|
sys.exit(app.exec_())
|
|
|
|
|
|
if __name__ == '__main__':
|
|
main()
|