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