feat(pmr): 라벨 인쇄 기능 구현 및 환산계수 개선

- Brother QL 라벨 인쇄 API 추가 (POST /pmr/api/label/print)
- PMR 라벨 인쇄 버튼 동작 구현 (QL-810W)
- 환산계수 sung_code 프론트→백엔드 전달 추가
- 환산계수 모달 제품명 readonly 처리 (MSSQL 원본 보호)
- Pillow 10+ 호환성 패치 (ANTIALIAS → LANCZOS)
This commit is contained in:
thug0bin 2026-03-12 13:41:16 +09:00
parent 98d370104b
commit 17a29f05b8
2 changed files with 168 additions and 7 deletions

View File

@ -36,6 +36,10 @@ from pathlib import Path
from datetime import datetime, date
import logging
from PIL import Image, ImageDraw, ImageFont
# Pillow 10+ 호환성 패치 (brother_ql용)
if not hasattr(Image, 'ANTIALIAS'):
Image.ANTIALIAS = Image.Resampling.LANCZOS
import io
import base64
import os
@ -624,6 +628,101 @@ def preview_label():
return jsonify({'success': False, 'error': str(e)}), 500
# API: 라벨 인쇄 (Brother QL 프린터)
# ─────────────────────────────────────────────────────────────
@pmr_bp.route('/api/label/print', methods=['POST'])
def print_label():
"""
라벨 인쇄 (PIL 렌더링 Brother QL 프린터 전송)
Request Body:
- patient_name, med_name, dosage, frequency, duration, unit, sung_code
- printer: 프린터 선택 (선택, 기본값 '168')
- '121': QL-710W (192.168.0.121)
- '168': QL-810W (192.168.0.168)
"""
try:
from brother_ql.raster import BrotherQLRaster
from brother_ql.conversion import convert
from brother_ql.backends.helpers import send
data = request.get_json()
patient_name = data.get('patient_name', '')
med_name = data.get('med_name', '')
add_info = data.get('add_info', '')
dosage = float(data.get('dosage', 0))
frequency = int(data.get('frequency', 0))
duration = int(data.get('duration', 0))
unit = data.get('unit', '')
sung_code = data.get('sung_code', '')
printer = data.get('printer', '168') # 기본값: QL-810W
# 프린터 설정
if printer == '121':
printer_ip = '192.168.0.121'
printer_model = 'QL-710W'
else:
printer_ip = '192.168.0.168'
printer_model = 'QL-810W'
# 환산계수 조회
conversion_factor = None
if sung_code:
try:
from db.dbsetup import db_manager
cf_result = db_manager.get_conversion_factor(sung_code)
conversion_factor = cf_result.get('conversion_factor')
except Exception as cf_err:
logging.warning(f"환산계수 조회 실패 (무시): {cf_err}")
# 1. 라벨 이미지 생성
label_image = create_label_image(
patient_name=patient_name,
med_name=med_name,
add_info=add_info,
dosage=dosage,
frequency=frequency,
duration=duration,
unit=unit,
conversion_factor=conversion_factor
)
# 2. 이미지 90도 회전 (Brother QL이 세로 방향 기준이므로)
label_rotated = label_image.rotate(90, expand=True)
# 3. Brother QL 프린터로 전송
qlr = BrotherQLRaster(printer_model)
instructions = convert(
qlr=qlr,
images=[label_rotated],
label='29',
rotate='0',
threshold=70.0,
dither=False,
compress=False,
red=False,
dpi_600=False,
hq=True,
cut=True
)
send(instructions, printer_identifier=f"tcp://{printer_ip}:9100")
logging.info(f"[SUCCESS] PMR 라벨 인쇄 성공: {med_name}{printer_model}")
return jsonify({
'success': True,
'message': f'{med_name} 라벨 인쇄 완료 ({printer_model})',
'printer': printer_model
})
except ImportError as e:
logging.error(f"brother_ql 라이브러리 없음: {e}")
return jsonify({'success': False, 'error': 'brother_ql 라이브러리가 설치되지 않았습니다'}), 500
except Exception as e:
logging.error(f"라벨 인쇄 오류: {e}")
return jsonify({'success': False, 'error': str(e)}), 500
def normalize_medication_name(med_name):
"""
약품명 정제 - 밀리그램 등을 mg로 변환, 불필요한 부분 제거

View File

@ -2985,14 +2985,76 @@
document.getElementById('previewModal').style.display = 'none';
}
// 라벨 인쇄 (TODO: 구현)
function printLabels() {
const selected = Array.from(document.querySelectorAll('.med-check:checked')).map(c => c.dataset.code);
if (selected.length === 0) {
// 라벨 인쇄 (Brother QL 프린터)
async function printLabels() {
const checkboxes = document.querySelectorAll('.med-check:checked');
if (checkboxes.length === 0) {
alert('인쇄할 약품을 선택하세요');
return;
}
alert(`선택된 약품 ${selected.length}개 인쇄 기능은 추후 구현 예정입니다.\n\n${selected.join('\n')}`);
const patientName = document.querySelector('.patient-info h2')?.textContent?.trim() || '';
let printedCount = 0;
let failedCount = 0;
for (const checkbox of checkboxes) {
const tr = checkbox.closest('tr');
if (!tr) continue;
const cells = tr.querySelectorAll('td');
const medName = tr.dataset.medName || cells[1]?.querySelector('.med-name')?.textContent?.replace(/^\d+/, '').trim() || '';
const addInfo = tr.dataset.addInfo || '';
const sungCode = tr.dataset.sungCode || '';
const unit = tr.dataset.unit || '정';
// 용량 파싱 (1회 투약량)
const doseText = cells[2]?.textContent || '0';
const dosage = parseFloat(doseText.replace(/[^0-9.]/g, '')) || 0;
// 횟수 파싱
const freqText = cells[3]?.textContent || '0';
const frequency = parseInt(freqText.replace(/[^0-9]/g, '')) || 0;
// 일수 파싱
const durText = cells[4]?.textContent?.replace(/[^0-9]/g, '') || '0';
const duration = parseInt(durText) || 0;
try {
const res = await fetch('/pmr/api/label/print', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
patient_name: patientName,
med_name: medName,
add_info: addInfo,
dosage: dosage,
frequency: frequency,
duration: duration,
unit: unit,
sung_code: sungCode,
printer: '168' // 기본: QL-810W
})
});
const data = await res.json();
if (data.success) {
printedCount++;
console.log('Print success:', medName);
} else {
failedCount++;
console.error('Print failed:', medName, data.error);
}
} catch (err) {
failedCount++;
console.error('Print error:', medName, err);
}
}
if (failedCount === 0) {
alert(`✅ ${printedCount}개 라벨 인쇄 완료!`);
} else {
alert(`⚠️ 인쇄 완료: ${printedCount}개\n실패: ${failedCount}개`);
}
}
// ═══════════════════════════════════════════════════════════════════════════
@ -3424,8 +3486,8 @@
<input type="text" id="drysyrup_ingredient_name" placeholder="예: 아목시실린">
</div>
<div class="drysyrup-form-row">
<label>제품명</label>
<input type="text" id="drysyrup_product_name" placeholder="예: 오구멘틴듀오시럽">
<label>제품명 <span style="font-size:0.75rem;color:#6b7280;">(MSSQL 원본)</span></label>
<input type="text" id="drysyrup_product_name" placeholder="예: 오구멘틴듀오시럽" readonly style="background:#f3f4f6;cursor:not-allowed;">
</div>
<div class="drysyrup-form-row">
<label>환산계수 (g/ml)</label>