Files
pharmacy-pos-qr-system/docs/LABEL_PRINTING_GUIDE.md

13 KiB
Raw Blame History

라벨 인쇄 시스템 가이드

pharmacy-pos-qr-system의 라벨 인쇄/미리보기 기능 문서

작성일: 2026-03-18


📁 파일 구조

backend/
├── pmr_api.py                    # 처방전(PMR) 라벨 인쇄 API
├── qr_printer.py                 # 약품 QR 라벨 (바코드/가격)
├── utils/
│   ├── otc_label_printer.py      # OTC 용법 라벨 (가로형 와이드)
│   └── qr_label_printer.py       # QR 영수증 라벨 (마일리지용)
└── samples/
    └── print_label.py            # 처방전 라벨 핵심 함수 (참조용)

🖨️ 프린터 설정

용도 모델 IP 포트 용지
QR 라벨 (121) Brother QL-710W 192.168.0.121 9100 29mm 연속
OTC 라벨 (168) Brother QL-810W 192.168.0.168 9100 29mm 연속

1 처방전 라벨 (PMR)

파일 위치

  • API: backend/pmr_api.py
  • 엔드포인트: /pmr/api/label/preview, /pmr/api/label/print

미리보기 API

POST /pmr/api/label/preview
Content-Type: application/json

Request Body:

{
  "patient_name": "홍길동",
  "med_name": "아모잘탄정5/50mg",
  "add_info": "고혈압치료제",
  "dosage": 1.0,
  "frequency": 2,
  "duration": 30,
  "unit": "정",
  "sung_code": "123456TB"
}

Response:

{
  "success": true,
  "image": "data:image/png;base64,iVBORw0KGgo...",
  "conversion_factor": null,
  "storage_conditions": "실온보관"
}

인쇄 API

POST /pmr/api/label/print
Content-Type: application/json

Request Body:

{
  "patient_name": "홍길동",
  "med_name": "아모잘탄정5/50mg",
  "add_info": "고혈압치료제",
  "dosage": 1.0,
  "frequency": 2,
  "duration": 30,
  "unit": "정",
  "sung_code": "123456TB",
  "printer": "168",
  "orientation": "portrait"
}

Parameters:

파라미터 타입 필수 설명
patient_name string 환자명
med_name string 약품명
add_info string 효능/분류 (PRINT_TYPE)
dosage float 1회 복용량
frequency int 1일 복용 횟수 (1,2,3...)
duration int 복용 일수
unit string 단위 (정, 캡슐, mL, 포, g)
sung_code string 성분코드 (환산계수 조회용)
printer string "121" 또는 "168" (기본: 168)
orientation string "portrait" 또는 "landscape" (기본: portrait)

핵심 함수 (pmr_api.py)

def create_label_image(patient_name, med_name, add_info='', dosage=0, 
                       frequency=0, duration=0, unit='정', 
                       conversion_factor=None, storage_conditions='실온보관'):
    """
    라벨 이미지 생성 (29mm 용지 기준, 306x380px)
    
    Returns:
        PIL.Image: RGB 이미지
    """
def normalize_medication_name(med_name):
    """
    약품명 정제
    - 밀리그램 → mg
    - 마이크로그램 → μg
    - 밀리리터 → mL
    - 언더스코어 뒤 내용 제거
    """
def get_drug_unit(goods_name, sung_code):
    """
    SUNG_CODE 마지막 2자리로 단위 판별
    - TB, TA, TC... → "정"
    - CA, CH, CS... → "캡슐"
    - SS, SY, LQ... → "mL"
    - GA, GB, PD... → "포"
    """

2 OTC 용법 라벨 (가로형 와이드)

파일 위치

  • 모듈: backend/utils/otc_label_printer.py
  • API: backend/app.py

미리보기 API

POST /api/admin/otc-labels/preview
Content-Type: application/json

Request Body:

{
  "drug_name": "타이레놀정500mg",
  "effect": "해열·진통",
  "dosage_instruction": "1일 3회, 1회 1~2정 [식후 30분]",
  "usage_tip": "공복 복용 시 위장장애 주의"
}

Response:

{
  "success": true,
  "preview_url": "data:image/png;base64,iVBORw0KGgo..."
}

인쇄 API

POST /api/admin/otc-labels/print
Content-Type: application/json

Request Body:

{
  "drug_name": "타이레놀정500mg",
  "effect": "해열·진통",
  "dosage_instruction": "1일 3회, 1회 1~2정 [식후 30분]",
  "usage_tip": "공복 복용 시 위장장애 주의",
  "barcode": "8806436044814"
}

바코드로 인쇄 (간편)

GET /api/otc-label-print/<barcode>

예: GET /api/otc-label-print/8806436044814

DB의 otc_label_presets 테이블에서 미리 저장된 라벨 정보 사용

핵심 함수 (utils/otc_label_printer.py)

def create_otc_label_image(drug_name, effect="", dosage_instruction="", usage_tip=""):
    """
    OTC 용법 라벨 이미지 생성 (800 x 306px 가로형)
    
    레이아웃:
    - 효능: 중앙 상단 72pt (매우 크게!)
    - 약품명: 오른쪽 중간 36pt
    - 용법: 왼쪽 하단 40pt (체크박스 포함)
    - 약국명: 오른쪽 하단 테두리 박스
    
    Returns:
        PIL.Image: 1-bit 이미지 (흑백)
    """
def generate_preview_image(drug_name, effect="", dosage_instruction="", usage_tip=""):
    """
    미리보기용 Base64 PNG 이미지 반환
    
    Returns:
        str: "data:image/png;base64,..." 형태
    """
def print_otc_label(drug_name, effect="", dosage_instruction="", usage_tip=""):
    """
    Brother QL-810W (192.168.0.168)로 인쇄
    - 이미지 90도 회전 후 전송
    
    Returns:
        bool: 성공 여부
    """

3 약품 QR 라벨 (바코드/가격)

파일 위치

  • 모듈: backend/qr_printer.py
  • API: backend/app.py

미리보기 API

POST /api/qr-preview
Content-Type: application/json

Request Body:

{
  "drug_name": "벤포파워Z",
  "barcode": "8806418067510",
  "sale_price": 3000,
  "drug_code": "A12345678"
}

Response:

{
  "success": true,
  "preview_url": "data:image/png;base64,iVBORw0KGgo..."
}

인쇄 API

POST /api/qr-print
Content-Type: application/json

Request Body:

{
  "drug_name": "벤포파워Z",
  "barcode": "8806418067510",
  "sale_price": 3000,
  "drug_code": "A12345678"
}

핵심 함수 (qr_printer.py)

def create_drug_qr_label(drug_name, barcode, sale_price, drug_code=None, 
                         pharmacy_name='청춘약국'):
    """
    약품 QR 라벨 이미지 생성 (306 x 380px)
    
    구조:
    ┌─────────────────┐
    │   [QR CODE]     │  ← 바코드 기반 QR (130x130px)
    ├─────────────────┤
    │   약품명         │  ← 중앙 정렬 (최대 2줄)
    ├─────────────────┤
    │   ₩12,000       │  ← 판매가격
    ├─────────────────┤
    │   청 춘 약 국    │  ← 테두리 박스
    └─────────────────┘
    
    Returns:
        PIL.Image: 1-bit 이미지
    """
def print_drug_qr_label(drug_name, barcode, sale_price, drug_code=None, 
                        pharmacy_name='청춘약국'):
    """
    Brother QL-710W (192.168.0.121)로 인쇄
    
    Returns:
        dict: {"success": True/False, "message": "...", "error": "..."}
    """
def preview_qr_label(drug_name, barcode, sale_price, drug_code=None, 
                     pharmacy_name='청춘약국'):
    """
    미리보기용 Base64 PNG 반환
    
    Returns:
        str: "data:image/png;base64,..."
    """

4 QR 영수증 라벨 (마일리지용)

파일 위치

  • 모듈: backend/utils/qr_label_printer.py
  • API: backend/app.py

인쇄 API

POST /api/admin/qr/print
Content-Type: application/json

Request Body:

{
  "transaction_id": "20251024000042",
  "total_amount": 50000,
  "claimable_points": 1500,
  "transaction_time": "2025-10-24T14:30:00",
  "token_raw": "abc123",
  "printer": "brother"
}

Parameters:

파라미터 타입 설명
transaction_id string 거래 번호
total_amount float 결제 금액
claimable_points int 적립 예정 포인트
transaction_time string 거래 시간 (ISO 8601)
token_raw string QR URL 생성용 토큰
printer string "brother" 또는 "pos"

핵심 함수 (utils/qr_label_printer.py)

def create_qr_receipt_label(qr_url, transaction_id, total_amount, 
                            claimable_points, transaction_time):
    """
    QR 영수증 라벨 이미지 생성 (800 x 306px 가로형)
    
    레이아웃:
    ┌─────────────────────────────────────────────────────────────┐
    │  [청춘약국]                           [QR CODE]             │
    │  2025-10-24 14:30                     200x200px             │
    │  거래: 20251024000042                                       │
    │                                                             │
    │  결제금액: 50,000원                                         │
    │  적립예정: 1,500P                                           │
    │                                                             │
    │  QR 촬영하고 포인트 받으세요!                                │
    └─────────────────────────────────────────────────────────────┘
    
    Returns:
        PIL.Image: 1-bit 이미지
    """
def print_qr_label(qr_url, transaction_id, total_amount, claimable_points,
                   transaction_time, preview_mode=False):
    """
    QR 라벨 출력 또는 미리보기
    
    Args:
        preview_mode: True = 미리보기 (파일 저장), False = 인쇄
        
    Returns:
        preview_mode=True: (성공 여부, 이미지 파일 경로)
        preview_mode=False: 성공 여부 (bool)
    """

🔧 공통 유틸리티

지그재그 테두리 (절취선)

def draw_scissor_border(draw, width, height, edge_size=10, steps=20):
    """
    라벨 테두리에 톱니 모양 절취선 그리기
    
    Args:
        draw: ImageDraw 객체
        width: 라벨 너비
        height: 라벨 높이
        edge_size: 톱니 크기 (px)
        steps: 톱니 개수
    """

중앙 정렬 텍스트

def draw_centered_text(draw, text, y, font, max_width=None):
    """
    중앙 정렬된 텍스트 출력 (줄바꿈 지원)
    
    Returns:
        int: 다음 Y 위치
    """

📦 의존성

pillow>=10.0.0          # 이미지 처리
brother-ql>=0.9.4       # Brother QL 프린터 제어
qrcode[pil]>=7.0        # QR 코드 생성

Brother QL 라이브러리 사용법

from brother_ql.raster import BrotherQLRaster
from brother_ql.conversion import convert
from brother_ql.backends.helpers import send

# 1. Raster 객체 생성
qlr = BrotherQLRaster("QL-810W")

# 2. 이미지 변환 (29mm 라벨)
instructions = convert(
    qlr=qlr,
    images=[pil_image],
    label='29',
    rotate='0',
    threshold=70.0,
    dither=False,
    compress=False,
    red=False,
    dpi_600=False,
    hq=True,      # 고화질
    cut=True      # 자동 절단
)

# 3. 프린터 전송
send(instructions, printer_identifier="tcp://192.168.0.168:9100")

⚠️ 주의사항

가로형 라벨 (800x306px)

Brother QL은 세로 방향이 기준이므로, 가로형 라벨은 90도 회전 후 전송해야 함:

# 가로형 이미지 생성 (800 x 306)
label_img = create_wide_label(...)

# 90도 회전 (시계 반대방향)
label_img_rotated = label_img.rotate(90, expand=True)

# 전송
send(convert(..., images=[label_img_rotated], ...))

폰트 경로

Windows: C:/Windows/Fonts/malgunbd.ttf Linux: /usr/share/fonts/truetype/nanum/NanumGothicBold.ttf

폴백 처리 권장:

font_paths = [
    "C:/Windows/Fonts/malgunbd.ttf",
    "/usr/share/fonts/truetype/nanum/NanumGothicBold.ttf",
]
for path in font_paths:
    if os.path.exists(path):
        font = ImageFont.truetype(path, 32)
        break
else:
    font = ImageFont.load_default()

이미지 모드

Brother QL은 1-bit (흑백) 이미지 권장:

image = Image.new("1", (width, height), 1)  # 1 = 흰색
# 또는
image = image.convert('1')

📋 테이블 스키마 (SQLite)

otc_label_presets

OTC 라벨 프리셋 저장용:

CREATE TABLE IF NOT EXISTS otc_label_presets (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    barcode TEXT UNIQUE NOT NULL,
    drug_name TEXT NOT NULL,
    effect TEXT,
    dosage_instruction TEXT,
    usage_tip TEXT,
    print_count INTEGER DEFAULT 0,
    last_printed_at DATETIME,
    created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
    updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
);

🔗 관련 문서

  • docs/PHARMACY_DB_GUIDE.md - 약국 DB 조회 가이드
  • docs/ENCODING_GUIDE.md - 인코딩 문제 해결