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:
parent
98d370104b
commit
17a29f05b8
@ -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로 변환, 불필요한 부분 제거
|
||||
|
||||
@ -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>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user