pharmacy-pos-qr-system/backend/utils/pos_qr_printer.py
시골약사 2ec73dd73d feat: QR 프린터 선택 기능 추가 (Zebra + ESC/POS)
- ESC/POS QR 영수증 인쇄 함수 추가 (pos_qr_printer.py)
  - QR 코드 이미지를 ESC/POS 비트맵 래스터로 변환
  - 150x150px QR 코드 + 거래 정보 텍스트 인쇄
  - EUC-KR 인코딩으로 한글 지원
  - TCP 소켓으로 프린터 전송

- POS GUI에 프린터 선택 토글 버튼 추가
  - 🖨️ Zebra 라벨 ⇄ 🖨️ POS 영수증 전환
  - POS 모드 시 설정 버튼 표시 (IP/포트 설정)
  - 미리보기 모드는 Zebra 전용

- POSSettingsDialog 재사용 (pos_thermal.py 연동)
  - config.json에 POS 프린터 설정 저장
  - 테스트 인쇄 기능 활용

- QRGeneratorThread 프린터 모드 지원
  - printer_mode 매개변수 추가 ('zebra' or 'pos')
  - pos_config 설정 전달
  - 프린터별 분기 처리

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-29 20:00:42 +09:00

147 lines
4.6 KiB
Python

# pos_qr_printer.py
# ESC/POS 프린터로 QR 영수증 인쇄
import socket
import qrcode
from PIL import Image
from datetime import datetime
def print_qr_receipt_escpos(qr_url, transaction_id, total_amount,
claimable_points, transaction_time,
printer_ip, printer_port=9100):
"""
ESC/POS 프린터로 QR 영수증 인쇄
Args:
qr_url (str): QR 코드 URL
transaction_id (str): 거래번호
total_amount (float): 판매 금액
claimable_points (int): 적립 예정 포인트
transaction_time (datetime): 거래 시간
printer_ip (str): 프린터 IP 주소
printer_port (int): 프린터 포트 (기본 9100)
Returns:
bool: 성공 여부
"""
try:
# 1. QR 코드 이미지 생성 (150x150px)
qr = qrcode.QRCode(version=1, box_size=5, border=2)
qr.add_data(qr_url)
qr.make(fit=True)
qr_image = qr.make_image(fill_color="black", back_color="white")
qr_image = qr_image.resize((150, 150))
# 2. QR 이미지를 ESC/POS 비트맵으로 변환
qr_bitmap = image_to_raster(qr_image)
# 3. ESC/POS 명령어 조립
ESC = b'\x1b'
GS = b'\x1d'
commands = []
commands.append(ESC + b'@') # 프린터 초기화
commands.append(ESC + b'a\x01') # 중앙 정렬
# 헤더
commands.append(ESC + b'!\x10') # 크게
commands.append("청춘약국\n".encode('euc-kr'))
commands.append(ESC + b'!\x00') # 보통
commands.append("================================\n".encode('euc-kr'))
# 거래 정보
date_str = transaction_time.strftime('%Y-%m-%d %H:%M')
commands.append(f"거래일시: {date_str}\n".encode('euc-kr'))
commands.append(f"거래번호: {transaction_id}\n".encode('euc-kr'))
commands.append("\n".encode('euc-kr'))
# 금액 정보
commands.append(ESC + b'!\x10') # 크게
commands.append(f"결제금액: {total_amount:,.0f}\n".encode('euc-kr'))
commands.append(f"적립예정: {claimable_points:,}P\n".encode('euc-kr'))
commands.append(ESC + b'!\x00') # 보통
commands.append("================================\n".encode('euc-kr'))
commands.append("\n".encode('euc-kr'))
# QR 코드 인쇄
commands.append(qr_bitmap) # QR 비트맵 데이터
commands.append("\n".encode('euc-kr'))
# 안내 문구
commands.append(ESC + b'!\x08') # 작게
commands.append("QR 촬영하고 포인트 받으세요!\n".encode('euc-kr'))
commands.append(ESC + b'!\x00') # 보통
commands.append("\n\n\n".encode('euc-kr'))
commands.append(GS + b'V\x00') # 용지 커트
# 4. TCP 소켓으로 전송
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.settimeout(5)
sock.connect((printer_ip, printer_port))
sock.sendall(b''.join(commands))
sock.close()
print(f"[ESC/POS] 인쇄 완료: {printer_ip}:{printer_port}")
return True
except socket.timeout:
print(f"[ESC/POS] 연결 시간 초과: {printer_ip}:{printer_port}")
return False
except ConnectionRefusedError:
print(f"[ESC/POS] 연결 거부됨: {printer_ip}:{printer_port}")
return False
except Exception as e:
print(f"[ESC/POS] 인쇄 오류: {e}")
return False
def image_to_raster(image):
"""
PIL 이미지를 ESC/POS 비트맵 래스터 데이터로 변환
ESC/POS GS v 0 명령어 사용:
GS v 0 m xL xH yL yH d1...dk
Args:
image (PIL.Image): PIL 이미지 객체
Returns:
bytes: ESC/POS 래스터 비트맵 명령어
"""
# 이미지를 흑백으로 변환
image = image.convert('1') # 1-bit 흑백
width, height = image.size
# 바이트 정렬 (8픽셀 = 1바이트)
width_bytes = (width + 7) // 8
# 헤더 생성
GS = b'\x1d'
cmd = GS + b'v0' # 래스터 비트맵 모드
cmd += b'\x00' # 보통 모드
cmd += bytes([width_bytes & 0xFF, (width_bytes >> 8) & 0xFF]) # xL, xH
cmd += bytes([height & 0xFF, (height >> 8) & 0xFF]) # yL, yH
# 이미지 데이터 변환
pixels = image.load()
data = []
for y in range(height):
line = []
for x in range(0, width, 8):
byte = 0
for bit in range(8):
if x + bit < width:
if pixels[x + bit, y] == 0: # 검은색
byte |= (1 << (7 - bit))
line.append(byte)
data.extend(line)
cmd += bytes(data)
return cmd