feat: QR 토큰 품목 상세 전송 지원 (items 파라미터)

This commit is contained in:
thug0bin
2026-03-29 12:37:36 +09:00
parent 3871154509
commit 21e1c3adfa
13 changed files with 3955 additions and 52 deletions

View File

@@ -0,0 +1,564 @@
# 라벨 인쇄 시스템 가이드
> 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` - 인코딩 문제 해결

434
docs/OTC_LABEL_SYSTEM.md Normal file
View File

@@ -0,0 +1,434 @@
# OTC 용법 라벨 시스템
## 1. 시스템 개요
### OTC 라벨이란?
OTC(Over-The-Counter) 약품 판매 시 부착하는 **용법·용량 안내 라벨**입니다.
약사가 직접 설명하는 것 외에 시각적 보조 자료로, 복용 방법과 효능을 명확히 전달합니다.
### 전체 흐름
```
바코드 스캔 → POS 연동 → 웹 관리 페이지 → 미리보기 → Brother 프린터 출력
```
1. **POS에서 바코드 스캔** → URL 호출 (`?barcode=...&name=...`)
2. **관리 페이지 자동 로드** → 기존 프리셋이 있으면 채움
3. **효능/용법 입력** → 실시간 미리보기
4. **인쇄** → Brother QL-810W로 29mm 라벨 출력
5. **프리셋 저장** → 다음 번엔 바코드만 스캔하면 바로 인쇄
---
## 2. 아키텍처
### 2.1 시스템 구성도
```
┌─────────────────┐ ┌──────────────────────────────┐
│ POS (PIT3000) │────▶│ Flask 서버 (port 7001) │
│ 바코드 스캔 │ │ /admin/otc-labels │
└─────────────────┘ └───────────┬──────────────────┘
┌────────────────┼────────────────┐
▼ ▼ ▼
┌──────────────┐ ┌────────────┐ ┌──────────────┐
│ SQLite │ │ MSSQL │ │ Brother │
│ (프리셋 저장) │ │ (약품 정보) │ │ QL-810W │
│ mileage.db │ │ PM_DRUG │ │ 192.168.0.168│
└──────────────┘ └────────────┘ └──────────────┘
```
### 2.2 Flask 라우트 구조
```
/admin/otc-labels ← 관리 페이지 (HTML)
/api/admin/otc-labels ← 프리셋 목록 조회 / 등록·수정
/api/admin/otc-labels/<barcode> ← 단건 조회 / 삭제
/api/admin/otc-labels/preview ← 미리보기 이미지 생성
/api/admin/otc-labels/print ← 라벨 인쇄
/api/admin/otc-labels/search-mssql ← MSSQL 약품 검색
/api/otc-label-print/<barcode> ← 외부 GET 인쇄 (CORS 지원)
/api/otc-label-check ← 프리셋 존재 여부 일괄 확인
```
### 2.3 DB 연결
| DB | 용도 | 연결 방식 |
|---|---|---|
| **SQLite** | 라벨 프리셋 저장 | `db_manager.get_sqlite_connection()` |
| **MSSQL** | 약품 마스터 (CD_GOODS) | `db_manager.get_session('PM_DRUG')` |
- SQLite DB 경로: `backend/db/mileage.db`
- MSSQL 인스턴스: `192.168.0.4\PM2014`
### 2.4 프린터 연동
| 항목 | 값 |
|---|---|
| 프린터 | Brother QL-810W |
| IP | 192.168.0.168 |
| 포트 | 9100 (TCP) |
| 용지 | 29mm 연속 라벨 |
| 라이브러리 | `brother_ql` |
---
## 3. API 엔드포인트
### 3.1 관리 페이지
```
GET /admin/otc-labels
GET /admin/otc-labels?barcode=8806436003118&name=노바손크림
```
- URL 파라미터로 바코드/이름 전달 시 자동 로드
---
### 3.2 프리셋 목록 조회
```http
GET /api/admin/otc-labels
```
**응답 예시:**
```json
{
"success": true,
"count": 5,
"labels": [
{
"id": 1,
"barcode": "8806436003118",
"drug_code": "DR001",
"display_name": "노바손크림",
"effect": "습진, 피부염",
"dosage_instruction": "1일 2회, 환부에 얇게 도포",
"usage_tip": "눈 주위 사용 금지",
"use_wide_format": true,
"print_count": 12,
"last_printed_at": "2026-03-19 15:30:00",
"created_at": "...",
"updated_at": "..."
}
]
}
```
---
### 3.3 프리셋 단건 조회
```http
GET /api/admin/otc-labels/{barcode}
```
**응답 예시:**
```json
{
"success": true,
"exists": true,
"label": { /* */ }
}
```
**프리셋 없는 경우 (404):**
```json
{
"success": false,
"error": "등록된 프리셋이 없습니다.",
"exists": false
}
```
---
### 3.4 프리셋 등록/수정 (Upsert)
```http
POST /api/admin/otc-labels
Content-Type: application/json
{
"barcode": "8806436003118",
"drug_code": "DR001",
"display_name": "",
"effect": ", ",
"dosage_instruction": "1 2, ",
"usage_tip": " ",
"use_wide_format": true
}
```
**필수 필드:** `barcode`
**동작:** 바코드가 이미 존재하면 UPDATE, 없으면 INSERT
---
### 3.5 프리셋 삭제
```http
DELETE /api/admin/otc-labels/{barcode}
```
---
### 3.6 미리보기 이미지 생성
```http
POST /api/admin/otc-labels/preview
Content-Type: application/json
{
"drug_name": "",
"effect": ", ",
"dosage_instruction": "1 2, ",
"usage_tip": " "
}
```
**응답:**
```json
{
"success": true,
"preview_url": "data:image/png;base64,iVBORw0KGgo..."
}
```
---
### 3.7 라벨 인쇄
```http
POST /api/admin/otc-labels/print
Content-Type: application/json
{
"barcode": "8806436003118",
"drug_name": "",
"effect": ", ",
"dosage_instruction": "1 2",
"usage_tip": ""
}
```
**동작:** 인쇄 후 `print_count` 증가, `last_printed_at` 갱신
---
### 3.8 외부 GET 인쇄 (CORS 지원)
```http
GET /api/otc-label-print/{barcode}
```
- **프리셋 있음** → 해당 데이터로 즉시 인쇄
- **프리셋 없음** → 404 반환 (인쇄 안 함)
- POS 등 외부 시스템에서 URL 호출로 바로 인쇄 가능
---
### 3.9 MSSQL 약품 검색
```http
GET /api/admin/otc-labels/search-mssql?q=
```
**응답:**
```json
{
"success": true,
"count": 3,
"drugs": [
{
"drug_code": "DR001",
"barcode": "8806436003118",
"goods_name": "노바손크림30g",
"sale_price": 8500.0
}
]
}
```
**쿼리 대상:**
- `CD_GOODS.GoodsName` (약품명)
- `CD_GOODS.Barcode` (바코드)
- `CD_ITEM_UNIT_MEMBER.CD_CD_BARCODE` (포장 단위 바코드)
---
### 3.10 프리셋 존재 여부 일괄 확인
```http
GET /api/otc-label-check?barcodes=8806436003118,8806436058613
#
POST /api/otc-label-check
Content-Type: application/json
{
"barcodes": ["8806436003118", "8806436058613"]
}
```
**응답:**
```json
{
"success": true,
"total": 2,
"found": 1,
"results": {
"8806436003118": true,
"8806436058613": false
}
}
```
---
## 4. DB 스키마
### 4.1 테이블: `otc_label_presets` (SQLite)
| 컬럼 | 타입 | 설명 |
|---|---|---|
| `id` | INTEGER | PK, 자동 증가 |
| `barcode` | VARCHAR(20) | **UNIQUE**, 바코드 (실질적 PK) |
| `drug_code` | VARCHAR(20) | MSSQL DrugCode (참조용) |
| `display_name` | VARCHAR(100) | 표시 이름 (오버라이드) |
| `effect` | VARCHAR(100) | 효능 (예: "치통, 두통") |
| `dosage_instruction` | TEXT | 용법 (예: "1일 3회, 1회 1정") |
| `usage_tip` | TEXT | 부가 설명 |
| `use_wide_format` | BOOLEAN | 와이드 포맷 사용 여부 |
| `print_count` | INTEGER | 인쇄 횟수 (통계) |
| `last_printed_at` | DATETIME | 마지막 인쇄 시간 |
| `created_at` | DATETIME | 생성 시간 |
| `updated_at` | DATETIME | 수정 시간 |
**인덱스:**
- `idx_otc_label_barcode` (barcode)
- `idx_otc_label_drug_code` (drug_code)
---
### 4.2 MSSQL 테이블: `CD_GOODS` (약품 마스터)
검색 시 조회하는 테이블:
```sql
SELECT TOP 20
G.DrugCode,
COALESCE(NULLIF(G.Barcode, ''),
(SELECT TOP 1 CD_CD_BARCODE FROM CD_ITEM_UNIT_MEMBER WHERE DrugCode = G.DrugCode)
) AS Barcode,
G.GoodsName,
G.Saleprice
FROM CD_GOODS G
WHERE G.GoodsName LIKE '%검색어%'
OR G.Barcode LIKE '%검색어%'
OR G.DrugCode IN (SELECT DrugCode FROM CD_ITEM_UNIT_MEMBER WHERE CD_CD_BARCODE LIKE '%검색어%')
```
---
## 5. 라벨 이미지 생성
### 5.1 이미지 사양
| 항목 | 값 |
|---|---|
| 크기 | 800 × 306 px |
| 색상 | 1-bit (흑백) |
| 폰트 | 맑은 고딕 Bold (`malgunbd.ttf`) |
### 5.2 레이아웃
```
┌────────────────────────────────────────────┐
│ [효능 - 72pt, 굵게, 중앙 상단] │
│ │
│ □ 용법 - 40pt, 왼쪽 정렬 │
│ [약품명 36pt] │
│ □ 부가 설명 - 26pt ┌──────────┐ │
│ │ 청춘약국 │ │
│ └──────────┘ │
└────────────────────────────────────────────┘
```
### 5.3 인쇄 과정
1. PIL로 이미지 생성 (가로 800 × 세로 306)
2. 90도 회전 (Brother QL은 세로 기준)
3. `brother_ql` 라이브러리로 래스터 변환
4. TCP 9100 포트로 전송
---
## 6. 관련 파일 목록
### 6.1 핵심 파일
| 파일 | 역할 |
|---|---|
| `backend/app.py` | Flask 라우트 (7730~8200줄) |
| `backend/utils/otc_label_printer.py` | 이미지 생성 & 프린터 출력 |
| `backend/templates/admin_otc_labels.html` | 관리 페이지 UI |
| `backend/db/mileage_schema.sql` | 테이블 스키마 |
| `backend/db/mileage.db` | SQLite DB |
### 6.2 app.py 내 주요 함수
| 함수명 | 라인 | 설명 |
|---|---|---|
| `admin_otc_labels()` | 7735 | 관리 페이지 렌더링 |
| `api_get_otc_labels()` | 7741 | 목록 조회 |
| `api_get_otc_label()` | 7770 | 단건 조회 |
| `api_upsert_otc_label()` | 7801 | 등록/수정 |
| `api_delete_otc_label()` | 7848 | 삭제 |
| `api_preview_otc_label()` | 7868 | 미리보기 |
| `api_print_otc_label()` | 7900 | 인쇄 |
| `api_otc_label_print_by_barcode()` | 7948 | GET 인쇄 |
| `api_otc_label_check()` | 8039 | 일괄 확인 |
| `api_search_mssql_drug()` | 8117 | MSSQL 검색 |
### 6.3 otc_label_printer.py 함수
| 함수명 | 설명 |
|---|---|
| `create_otc_label_image()` | 라벨 이미지 생성 (PIL) |
| `print_otc_label()` | Brother QL로 인쇄 |
| `generate_preview_image()` | Base64 미리보기 생성 |
---
## 7. 트러블슈팅
### 프린터 연결 테스트
```python
import socket
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.settimeout(3)
result = sock.connect_ex(("192.168.0.168", 9100))
print("OK" if result == 0 else f"FAIL: {result}")
sock.close()
```
### 모듈 로드 실패
`OTC_LABEL_AVAILABLE = False` 로그 발생 시:
- `brother_ql` 설치 확인: `pip install brother_ql`
- Pillow 설치 확인: `pip install Pillow`
### 폰트 깨짐
- Windows: `C:/Windows/Fonts/malgunbd.ttf` 존재 확인
- 대체 폰트: NanumGothicBold 등
---
## 8. 사용 예시
### POS에서 라벨 페이지 열기
```
https://mile.0bin.in/admin/otc-labels?barcode=8806436003118&name=노바손크림
```
### 외부 시스템에서 바로 인쇄
```bash
curl https://mile.0bin.in/api/otc-label-print/8806436003118
```
### 프리셋 일괄 등록 (스크립트)
```python
import requests
labels = [
{"barcode": "8806436003118", "display_name": "노바손크림", "effect": "습진", "dosage_instruction": "1일 2회"},
{"barcode": "8806436058613", "display_name": "게보린", "effect": "두통", "dosage_instruction": "1회 1정"},
]
for label in labels:
requests.post("https://mile.0bin.in/api/admin/otc-labels", json=label)
```
---
*문서 작성: 2026-03-19*

263
docs/환산계수.md Normal file
View File

@@ -0,0 +1,263 @@
# 건조시럽 환산계수 시스템
> 작성일: 2026-03-19
> 작성자: 용림 🐉
---
## 1. 개요
건조시럽(dry syrup)은 물로 희석하여 복용하는 시럽 형태의 의약품입니다.
**환산계수(conversion_factor)**를 사용하여 복용량(mL)을 실제 분말량(g)으로 변환합니다.
### 계산 예시
```
오구멘틴듀오시럽 228mg/5ml
├─ 환산계수: 0.11
├─ 총량: 120mL
└─ 필요 분말량: 120 × 0.11 = 13.2g
```
---
## 2. 데이터베이스 정보
### PostgreSQL 연결
| 항목 | 값 |
|------|-----|
| **Host** | 192.168.0.39 |
| **Port** | 5432 |
| **Database** | label10 |
| **User** | admin |
| **Password** | trajet6640 |
### Connection String
```
postgresql://admin:trajet6640@192.168.0.39:5432/label10
```
### Python 연결 코드
```python
import psycopg2
conn = psycopg2.connect(
host='192.168.0.39',
port=5432,
database='label10',
user='admin',
password='trajet6640'
)
```
---
## 3. 테이블 스키마
### drysyrup 테이블
| 컬럼명 | 타입 | 설명 |
|--------|------|------|
| `idx` | INTEGER | PK, 자동증가 |
| `ingredient_code` | VARCHAR | 성분코드 (MSSQL SUNG_CODE와 매칭) |
| `ingredient_name` | VARCHAR | 성분명 |
| `product_name` | VARCHAR | 대표 제품명 |
| `post_prep_amount` | VARCHAR | 조제 후 농도 (예: 25mg/ml) |
| `main_ingredient_amt` | VARCHAR | 주성분량 (예: 0.75g/16.7g) |
| `conversion_factor` | DOUBLE PRECISION | **환산계수** (mL → g) |
| `storage_conditions` | VARCHAR | 보관조건 (냉장, 상온 등) |
| `expiration_date` | VARCHAR | 조제 후 유효기간 |
### 매핑 관계
```
MSSQL (PIT3000) PostgreSQL (label10)
───────────────── ────────────────────
PM_DRUG.CD_GOODS drysyrup
└─ SUNG_CODE ──────▶ └─ ingredient_code
```
---
## 4. 데이터 샘플 (23건)
| idx | ingredient_code | 성분명 | 제품명 | 환산계수 | 보관 | 유효기간 |
|-----|-----------------|--------|--------|----------|------|----------|
| 18 | 125333ASY | 세파드록실수화물 | 보령듀리세프 125mg/5ml | 0.557 | 냉장 | 14일 |
| 19 | 125332ASY | 세파드록실수화물 | 보령듀리세프 250mg/5ml | 0.557 | 냉장 | 14일 |
| 20 | 125237ASY | 세파클러수화물 | 크로세프 | 0.667 | 냉장 | 14일 |
| 21 | 128931ASY | 세푸록심악세틸 | 올세프 | 1.0 | 25℃이하 | 10일 |
| 22 | 127931ASY | 세프포독심프록세틸 | 포독스 | 0.2 | 냉장 | 14일 |
| 23 | 128030ASY | 세프프로질수화물 | 세프질시럽 | 0.5 | 냉장 | 14일 |
| 24 | 108130ASY | 아목시실린수화물 | 파목신시럽 | 0.775 | 냉장 | 14일 |
| 25 | 535000ASY | 아목시실린+클라불란산 | 오구멘틴듀오 228mg/5ml | **0.11** | 냉장 | 7일 |
| 26 | 536300ASY | 아목시실린+클라불란산 | 아목클란네오시럽 | 0.22 | 냉장 | 7일 |
| 27 | 112732ASY | 아지트로마이신수화물 | 지스로맥스 | 0.867 | 상온 | 5일 |
---
## 5. API 엔드포인트
### 환산계수 조회
```
GET /api/drug-info/conversion-factor/<sung_code>
```
#### 요청 예시
```bash
curl https://mile.0bin.in/api/drug-info/conversion-factor/535000ASY
```
#### 응답 (성공)
```json
{
"success": true,
"sung_code": "535000ASY",
"conversion_factor": 0.11,
"ingredient_name": "아목시실린수화물·클라불란산칼륨",
"product_name": "일성오구멘틴듀오시럽 228mg/5ml"
}
```
#### 응답 (데이터 없음)
```json
{
"success": true,
"sung_code": "NOTEXIST",
"conversion_factor": null,
"ingredient_name": null,
"product_name": null
}
```
---
## 6. 쿼리 예시
### 환산계수 조회
```sql
SELECT conversion_factor, ingredient_name, product_name,
storage_conditions, expiration_date
FROM drysyrup
WHERE ingredient_code = '535000ASY';
```
### 전체 목록 조회
```sql
SELECT * FROM drysyrup ORDER BY idx;
```
### 특정 성분 검색
```sql
SELECT * FROM drysyrup
WHERE ingredient_name LIKE '%아목시실린%';
```
---
## 7. Python 사용 예시
### 환산계수 조회 함수
```python
import psycopg2
def get_conversion_factor(sung_code):
"""성분코드로 환산계수 조회"""
conn = psycopg2.connect(
host='192.168.0.39',
port=5432,
database='label10',
user='admin',
password='trajet6640'
)
cursor = conn.cursor()
cursor.execute("""
SELECT conversion_factor, ingredient_name, product_name,
storage_conditions, expiration_date
FROM drysyrup
WHERE ingredient_code = %s
""", (sung_code,))
row = cursor.fetchone()
conn.close()
if row:
return {
'conversion_factor': row[0],
'ingredient_name': row[1],
'product_name': row[2],
'storage_conditions': row[3],
'expiration_date': row[4]
}
return None
# 사용 예시
result = get_conversion_factor('535000ASY')
print(result)
# {'conversion_factor': 0.11, 'ingredient_name': '아목시실린...', ...}
```
### 분말량 계산 함수
```python
def calculate_powder_amount(sung_code, total_ml):
"""총 mL로 필요한 분말량(g) 계산"""
data = get_conversion_factor(sung_code)
if data and data['conversion_factor']:
return round(total_ml * data['conversion_factor'], 2)
return None
# 사용 예시
powder = calculate_powder_amount('535000ASY', 120)
print(f"필요 분말량: {powder}g") # 필요 분말량: 13.2g
```
---
## 8. 관련 파일
| 파일 | 위치 | 설명 |
|------|------|------|
| app.py | `backend/app.py` | Flask API 라우트 |
| DRYSYRUP_CONVERSION.md | `docs/` | 기존 문서 |
### Flask 라우트 위치
```python
# backend/app.py
@app.route('/api/drug-info/conversion-factor/<sung_code>')
def get_drug_conversion_factor(sung_code):
...
```
---
## 9. 아키텍처
```
┌─────────────────────────────────────────────────────────┐
│ 클라이언트 │
│ (POS, 라벨 프린터, 웹 UI) │
└─────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────┐
│ Flask Backend (7001) │
│ GET /api/drug-info/conversion-factor/<sung_code> │
└─────────────────────────────────────────────────────────┘
┌───────────────┴───────────────┐
▼ ▼
┌─────────────────────┐ ┌─────────────────────┐
│ MSSQL (PIT3000) │ │ PostgreSQL │
│ 192.168.0.4 │ │ 192.168.0.39:5432 │
├─────────────────────┤ ├─────────────────────┤
│ PM_DRUG.CD_GOODS │ │ label10.drysyrup │
│ └─ SUNG_CODE ─────┼──────▶│ └─ ingredient_code│
│ └─ GoodsName │ │ └─ conversion_factor
│ └─ DrugCode │ │ └─ storage_conditions
└─────────────────────┘ └─────────────────────┘
```
---
*총 23개 건조시럽 환산계수 등록됨*