""" QR 영수증 라벨 인쇄 모듈 Brother QL-810W 프린터용 29mm 가로형 라벨 """ from PIL import Image, ImageDraw, ImageFont from brother_ql.raster import BrotherQLRaster from brother_ql.conversion import convert from brother_ql.backends.helpers import send import qrcode import os from datetime import datetime import logging # 프린터 설정 (barcode_print.py와 동일) PRINTER_IP = "192.168.0.168" PRINTER_PORT = 9100 PRINTER_MODEL = "QL-810W" LABEL_TYPE = "29" # 로깅 설정 logging.basicConfig(level=logging.INFO, format='[QR_LABEL] %(levelname)s: %(message)s') def get_font_path(): """ 폰트 파일 경로 자동 감지 (폴백 지원) Returns: str: 폰트 파일 경로 또는 None (기본 폰트 사용) """ candidates = [ os.path.join(os.path.dirname(__file__), "..", "samples", "fonts", "malgunbd.ttf"), "C:\\Windows\\Fonts\\malgunbd.ttf", # Windows 기본 경로 "C:\\Windows\\Fonts\\malgun.ttf", # 볼드 아닌 버전 "/usr/share/fonts/truetype/malgun.ttf", # Linux "malgun.ttf", # 시스템 폰트 ] for path in candidates: if os.path.exists(path): logging.info(f"폰트 파일 사용: {path}") return path logging.warning("폰트 파일을 찾을 수 없습니다. 기본 폰트 사용.") return None def create_qr_receipt_label(qr_url, transaction_id, total_amount, claimable_points, transaction_time): """ QR 영수증 라벨 이미지 생성 Args: qr_url (str): QR 코드 URL (token_raw 포함) transaction_id (str): 거래 번호 total_amount (float): 총 판매 금액 claimable_points (int): 적립 가능 포인트 transaction_time (datetime): 거래 시간 Returns: PIL.Image: 생성된 라벨 이미지 (800x306px, mode='1') 레이아웃 (가로형): ┌────────────────────────────────────────────────────────────┐ │ │ │ [청춘약국] [QR CODE] │ │ 2025-10-24 14:30 120x120px │ │ 거래: 20251024000042 │ │ │ │ 결제금액: 50,000원 │ │ 적립예정: 1,500P │ │ │ │ QR 촬영하고 포인트 받으세요! │ │ │ └────────────────────────────────────────────────────────────┘ """ try: # 1. 캔버스 생성 (가로형) width = 800 height = 306 img = Image.new('1', (width, height), 1) # 흰색 배경 draw = ImageDraw.Draw(img) # 2. 폰트 로드 font_path = get_font_path() try: if font_path: font_title = ImageFont.truetype(font_path, 48) # 약국명 font_info = ImageFont.truetype(font_path, 32) # 거래 정보 font_amount = ImageFont.truetype(font_path, 40) # 금액 (크게) font_points = ImageFont.truetype(font_path, 36) # 포인트 (강조) font_small = ImageFont.truetype(font_path, 28) # 안내 문구 font_tiny = ImageFont.truetype(font_path, 18) # 개인정보 동의 (작게) else: raise IOError("폰트 없음") except (IOError, OSError): logging.warning("TrueType 폰트 로드 실패. 기본 폰트 사용.") font_title = ImageFont.load_default() font_info = ImageFont.load_default() font_amount = ImageFont.load_default() font_points = ImageFont.load_default() font_small = ImageFont.load_default() font_tiny = ImageFont.load_default() # 3. QR 코드 생성 (우측 상단) - 크기 및 해상도 개선 qr = qrcode.QRCode( version=1, error_correction=qrcode.constants.ERROR_CORRECT_H, # 최고 레벨 (30% 복원) box_size=8, # 기존 4 -> 8로 증가 (더 선명) border=2, # 테두리 2칸 (인식률 향상) ) qr.add_data(qr_url) qr.make(fit=True) qr_img = qr.make_image(fill_color="black", back_color="white") # QR 코드 크기 및 위치 (기존 120 -> 200으로 증가) qr_size = 200 # 크기 대폭 증가 qr_img = qr_img.resize((qr_size, qr_size), Image.LANCZOS) qr_x = width - qr_size - 15 # 우측 여백 15px qr_y = 10 # 상단 여백 10px # QR 코드 붙이기 if qr_img.mode != '1': qr_img = qr_img.convert('1') img.paste(qr_img, (qr_x, qr_y)) # 4. 좌측 텍스트 영역 (y 위치 추적) x_margin = 20 y = 20 # 약국명 (굵게) pharmacy_text = "청춘약국" for offset in [(0, 0), (1, 0), (2, 0), (0, 1), (1, 1), (2, 1)]: draw.text((x_margin + offset[0], y + offset[1]), pharmacy_text, font=font_title, fill=0) y += 55 # 거래 시간 time_text = transaction_time.strftime('%Y-%m-%d %H:%M') draw.text((x_margin, y), time_text, font=font_info, fill=0) y += 40 # 거래 번호 tx_text = f"거래: {transaction_id}" draw.text((x_margin, y), tx_text, font=font_info, fill=0) y += 50 # 결제 금액 (강조) amount_text = f"결제금액: {int(total_amount):,}원" for offset in [(0, 0), (1, 0), (0, 1), (1, 1)]: draw.text((x_margin + offset[0], y + offset[1]), amount_text, font=font_amount, fill=0) y += 50 # 적립 포인트 (굵게) points_text = f"적립예정: {claimable_points:,}P" for offset in [(0, 0), (1, 0), (0, 1), (1, 1)]: draw.text((x_margin + offset[0], y + offset[1]), points_text, font=font_points, fill=0) y += 55 # 안내 문구 guide_text = "QR 촬영하고 포인트 받으세요!" draw.text((x_margin, y), guide_text, font=font_small, fill=0) y += 35 # 개인정보 동의 안내 (작은 글씨) privacy_text = "(QR 스캔 시 개인정보 수집·이용에 동의한 것으로 간주됩니다)" draw.text((x_margin, y), privacy_text, font=font_tiny, fill=0) # 5. 테두리 (가위선 스타일) for i in range(2): draw.rectangle([5 + i, 5 + i, width - 5 - i, height - 5 - i], outline=0) logging.info(f"QR 라벨 이미지 생성 완료: {transaction_id}") return img except Exception as e: logging.error(f"QR 라벨 이미지 생성 실패: {e}") raise def print_qr_label(qr_url, transaction_id, total_amount, claimable_points, transaction_time, preview_mode=False): """ QR 라벨 출력 또는 미리보기 Args: qr_url (str): QR 코드 URL transaction_id (str): 거래 번호 total_amount (float): 판매 금액 claimable_points (int): 적립 포인트 transaction_time (datetime): 거래 시간 preview_mode (bool): True = 미리보기, False = 인쇄 Returns: preview_mode=True: (성공 여부, 이미지 파일 경로) preview_mode=False: 성공 여부 (bool) """ try: logging.info(f"QR 라벨 {'미리보기' if preview_mode else '출력'} 시작: {transaction_id}") # 1. 라벨 이미지 생성 label_image = create_qr_receipt_label( qr_url, transaction_id, total_amount, claimable_points, transaction_time ) # 2. 미리보기 모드 if preview_mode: # temp 디렉터리 생성 temp_dir = os.path.join(os.path.dirname(__file__), "..", "samples", "temp") os.makedirs(temp_dir, exist_ok=True) # 파일명 생성 timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") filename = f"qr_receipt_{transaction_id}_{timestamp}.png" file_path = os.path.join(temp_dir, filename) # PNG로 저장 label_image.save(file_path, "PNG") logging.info(f"미리보기 이미지 저장: {file_path}") return True, file_path # 3. 실제 인쇄 모드 else: # 이미지 90도 회전 (Brother QL은 세로 기준) label_image_rotated = label_image.rotate(90, expand=True) # Brother QL Raster 변환 qlr = BrotherQLRaster(PRINTER_MODEL) instructions = convert( qlr=qlr, images=[label_image_rotated], label=LABEL_TYPE, rotate="0", threshold=70.0, dither=False, compress=False, red=False, dpi_600=False, hq=True, cut=True ) # 프린터로 전송 printer_identifier = f"tcp://{PRINTER_IP}:{PRINTER_PORT}" send(instructions, printer_identifier=printer_identifier) logging.info(f"QR 라벨 출력 완료: {transaction_id}") return True except Exception as e: logging.error(f"QR 라벨 {'미리보기' if preview_mode else '출력'} 실패: {e}") if preview_mode: return False, None else: return False # 테스트 코드 if __name__ == "__main__": # 테스트 데이터 test_qr_url = "https://pharmacy.example.com/claim?t=20251024000042:abc123:2025-10-24T14:30:00" test_tx_id = "20251024000042" test_amount = 50000.0 test_points = 1500 test_time = datetime.now() print("=" * 80) print("QR 라벨 생성 테스트") print("=" * 80) print(f"거래 ID: {test_tx_id}") print(f"금액: {test_amount:,}원") print(f"적립: {test_points}P") print(f"QR URL: {test_qr_url[:60]}...") print("=" * 80) # 미리보기 테스트 print("\n미리보기 모드 테스트...") success, image_path = print_qr_label( test_qr_url, test_tx_id, test_amount, test_points, test_time, preview_mode=True ) if success: print(f"[OK] 테스트 성공!") print(f"이미지 저장: {image_path}") print(f"\n다음 명령으로 확인:") print(f" start {image_path}") else: print("[ERROR] 테스트 실패")