13 KiB
13 KiB
라벨 인쇄 시스템 가이드
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- 인코딩 문제 해결