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

565 lines
13 KiB
Markdown
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 라벨 인쇄 시스템 가이드
> 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/<barcode>
```
예: `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` - 인코딩 문제 해결