# 라벨 인쇄 시스템 가이드

> 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` - 인코딩 문제 해결
