# 라벨 인쇄 시스템 가이드 > 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:** ```json { "patient_name": "홍길동", "med_name": "아모잘탄정5/50mg", "add_info": "고혈압치료제", "dosage": 1.0, "frequency": 2, "duration": 30, "unit": "정", "sung_code": "123456TB" } ``` **Response:** ```json { "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:** ```json { "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`) ```python 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 이미지 """ ``` ```python def normalize_medication_name(med_name): """ 약품명 정제 - 밀리그램 → mg - 마이크로그램 → μg - 밀리리터 → mL - 언더스코어 뒤 내용 제거 """ ``` ```python 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:** ```json { "drug_name": "타이레놀정500mg", "effect": "해열·진통", "dosage_instruction": "1일 3회, 1회 1~2정 [식후 30분]", "usage_tip": "공복 복용 시 위장장애 주의" } ``` **Response:** ```json { "success": true, "preview_url": "data:image/png;base64,iVBORw0KGgo..." } ``` ### 인쇄 API ``` POST /api/admin/otc-labels/print Content-Type: application/json ``` **Request Body:** ```json { "drug_name": "타이레놀정500mg", "effect": "해열·진통", "dosage_instruction": "1일 3회, 1회 1~2정 [식후 30분]", "usage_tip": "공복 복용 시 위장장애 주의", "barcode": "8806436044814" } ``` ### 바코드로 인쇄 (간편) ``` GET /api/otc-label-print/ ``` 예: `GET /api/otc-label-print/8806436044814` > DB의 `otc_label_presets` 테이블에서 미리 저장된 라벨 정보 사용 ### 핵심 함수 (`utils/otc_label_printer.py`) ```python def create_otc_label_image(drug_name, effect="", dosage_instruction="", usage_tip=""): """ OTC 용법 라벨 이미지 생성 (800 x 306px 가로형) 레이아웃: - 효능: 중앙 상단 72pt (매우 크게!) - 약품명: 오른쪽 중간 36pt - 용법: 왼쪽 하단 40pt (체크박스 포함) - 약국명: 오른쪽 하단 테두리 박스 Returns: PIL.Image: 1-bit 이미지 (흑백) """ ``` ```python def generate_preview_image(drug_name, effect="", dosage_instruction="", usage_tip=""): """ 미리보기용 Base64 PNG 이미지 반환 Returns: str: "data:image/png;base64,..." 형태 """ ``` ```python 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:** ```json { "drug_name": "벤포파워Z", "barcode": "8806418067510", "sale_price": 3000, "drug_code": "A12345678" } ``` **Response:** ```json { "success": true, "preview_url": "data:image/png;base64,iVBORw0KGgo..." } ``` ### 인쇄 API ``` POST /api/qr-print Content-Type: application/json ``` **Request Body:** ```json { "drug_name": "벤포파워Z", "barcode": "8806418067510", "sale_price": 3000, "drug_code": "A12345678" } ``` ### 핵심 함수 (`qr_printer.py`) ```python 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 이미지 """ ``` ```python 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": "..."} """ ``` ```python 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:** ```json { "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`) ```python 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 이미지 """ ``` ```python 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) """ ``` --- ## 🔧 공통 유틸리티 ### 지그재그 테두리 (절취선) ```python def draw_scissor_border(draw, width, height, edge_size=10, steps=20): """ 라벨 테두리에 톱니 모양 절취선 그리기 Args: draw: ImageDraw 객체 width: 라벨 너비 height: 라벨 높이 edge_size: 톱니 크기 (px) steps: 톱니 개수 """ ``` ### 중앙 정렬 텍스트 ```python 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 라이브러리 사용법 ```python 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도 회전 후 전송**해야 함: ```python # 가로형 이미지 생성 (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` 폴백 처리 권장: ```python 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 (흑백)** 이미지 권장: ```python image = Image.new("1", (width, height), 1) # 1 = 흰색 # 또는 image = image.convert('1') ``` --- ## 📋 테이블 스키마 (SQLite) ### otc_label_presets OTC 라벨 프리셋 저장용: ```sql 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` - 인코딩 문제 해결