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
|
from datetime import datetime, date
|
||||||
import logging
|
import logging
|
||||||
from PIL import Image, ImageDraw, ImageFont
|
from PIL import Image, ImageDraw, ImageFont
|
||||||
|
|
||||||
|
# Pillow 10+ 호환성 패치 (brother_ql용)
|
||||||
|
if not hasattr(Image, 'ANTIALIAS'):
|
||||||
|
Image.ANTIALIAS = Image.Resampling.LANCZOS
|
||||||
import io
|
import io
|
||||||
import base64
|
import base64
|
||||||
import os
|
import os
|
||||||
@ -624,6 +628,101 @@ def preview_label():
|
|||||||
return jsonify({'success': False, 'error': str(e)}), 500
|
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):
|
def normalize_medication_name(med_name):
|
||||||
"""
|
"""
|
||||||
약품명 정제 - 밀리그램 등을 mg로 변환, 불필요한 부분 제거
|
약품명 정제 - 밀리그램 등을 mg로 변환, 불필요한 부분 제거
|
||||||
|
|||||||
@ -2985,14 +2985,76 @@
|
|||||||
document.getElementById('previewModal').style.display = 'none';
|
document.getElementById('previewModal').style.display = 'none';
|
||||||
}
|
}
|
||||||
|
|
||||||
// 라벨 인쇄 (TODO: 구현)
|
// 라벨 인쇄 (Brother QL 프린터)
|
||||||
function printLabels() {
|
async function printLabels() {
|
||||||
const selected = Array.from(document.querySelectorAll('.med-check:checked')).map(c => c.dataset.code);
|
const checkboxes = document.querySelectorAll('.med-check:checked');
|
||||||
if (selected.length === 0) {
|
if (checkboxes.length === 0) {
|
||||||
alert('인쇄할 약품을 선택하세요');
|
alert('인쇄할 약품을 선택하세요');
|
||||||
return;
|
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="예: 아목시실린">
|
<input type="text" id="drysyrup_ingredient_name" placeholder="예: 아목시실린">
|
||||||
</div>
|
</div>
|
||||||
<div class="drysyrup-form-row">
|
<div class="drysyrup-form-row">
|
||||||
<label>제품명</label>
|
<label>제품명 <span style="font-size:0.75rem;color:#6b7280;">(MSSQL 원본)</span></label>
|
||||||
<input type="text" id="drysyrup_product_name" placeholder="예: 오구멘틴듀오시럽">
|
<input type="text" id="drysyrup_product_name" placeholder="예: 오구멘틴듀오시럽" readonly style="background:#f3f4f6;cursor:not-allowed;">
|
||||||
</div>
|
</div>
|
||||||
<div class="drysyrup-form-row">
|
<div class="drysyrup-form-row">
|
||||||
<label>환산계수 (g/ml)</label>
|
<label>환산계수 (g/ml)</label>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user