feat: PMR 라벨 미리보기 기능
- /pmr/api/label/preview: PIL 렌더링 → Base64 이미지 - 미리보기 버튼 + 모달 추가 - 29mm 용지 기준 라벨 이미지 생성
This commit is contained in:
parent
8d025457c0
commit
c21aa956da
@ -1,10 +1,14 @@
|
||||
# pmr_api.py - 조제관리(PMR) Blueprint API
|
||||
# PharmaIT3000 MSSQL 연동 (192.168.0.4)
|
||||
|
||||
from flask import Blueprint, jsonify, request, render_template
|
||||
from flask import Blueprint, jsonify, request, render_template, send_file
|
||||
import pyodbc
|
||||
from datetime import datetime, date
|
||||
import logging
|
||||
from PIL import Image, ImageDraw, ImageFont
|
||||
import io
|
||||
import base64
|
||||
import os
|
||||
|
||||
pmr_bp = Blueprint('pmr', __name__, url_prefix='/pmr')
|
||||
|
||||
@ -319,3 +323,191 @@ def test_connection():
|
||||
})
|
||||
except Exception as e:
|
||||
return jsonify({'success': False, 'error': str(e)}), 500
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
# API: 라벨 미리보기
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
@pmr_bp.route('/api/label/preview', methods=['POST'])
|
||||
def preview_label():
|
||||
"""
|
||||
라벨 미리보기 (PIL 렌더링 → Base64 이미지)
|
||||
|
||||
Request Body:
|
||||
- patient_name: 환자명
|
||||
- med_name: 약품명
|
||||
- dosage: 1회 복용량
|
||||
- frequency: 복용 횟수
|
||||
- duration: 복용 일수
|
||||
- unit: 단위 (정, 캡슐, mL 등)
|
||||
"""
|
||||
try:
|
||||
data = request.get_json()
|
||||
|
||||
patient_name = data.get('patient_name', '')
|
||||
med_name = data.get('med_name', '')
|
||||
dosage = float(data.get('dosage', 0))
|
||||
frequency = int(data.get('frequency', 0))
|
||||
duration = int(data.get('duration', 0))
|
||||
unit = data.get('unit', '정')
|
||||
|
||||
# 라벨 이미지 생성
|
||||
image = create_label_image(
|
||||
patient_name=patient_name,
|
||||
med_name=med_name,
|
||||
dosage=dosage,
|
||||
frequency=frequency,
|
||||
duration=duration,
|
||||
unit=unit
|
||||
)
|
||||
|
||||
# Base64 인코딩
|
||||
buffer = io.BytesIO()
|
||||
image.save(buffer, format='PNG')
|
||||
buffer.seek(0)
|
||||
img_base64 = base64.b64encode(buffer.getvalue()).decode('utf-8')
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'image': f'data:image/png;base64,{img_base64}'
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
logging.error(f"라벨 미리보기 오류: {e}")
|
||||
return jsonify({'success': False, 'error': str(e)}), 500
|
||||
|
||||
|
||||
def create_label_image(patient_name, med_name, dosage, frequency, duration, unit='정'):
|
||||
"""
|
||||
라벨 이미지 생성 (29mm 용지 기준)
|
||||
"""
|
||||
# 라벨 크기 (29mm 용지, 300dpi 기준)
|
||||
label_width = 306
|
||||
label_height = 380
|
||||
|
||||
image = Image.new("RGB", (label_width, label_height), "white")
|
||||
draw = ImageDraw.Draw(image)
|
||||
|
||||
# 폰트 설정 (Windows 경로)
|
||||
font_path = "C:/Windows/Fonts/malgunbd.ttf"
|
||||
if not os.path.exists(font_path):
|
||||
font_path = "C:/Windows/Fonts/malgun.ttf"
|
||||
|
||||
try:
|
||||
name_font = ImageFont.truetype(font_path, 36)
|
||||
drug_font = ImageFont.truetype(font_path, 24)
|
||||
info_font = ImageFont.truetype(font_path, 22)
|
||||
small_font = ImageFont.truetype(font_path, 18)
|
||||
except:
|
||||
name_font = ImageFont.load_default()
|
||||
drug_font = ImageFont.load_default()
|
||||
info_font = ImageFont.load_default()
|
||||
small_font = ImageFont.load_default()
|
||||
|
||||
# 중앙 정렬 텍스트 함수
|
||||
def draw_centered(text, y, font, fill="black"):
|
||||
bbox = draw.textbbox((0, 0), text, font=font)
|
||||
w = bbox[2] - bbox[0]
|
||||
x = (label_width - w) // 2
|
||||
draw.text((x, y), text, font=font, fill=fill)
|
||||
return y + bbox[3] - bbox[1] + 5
|
||||
|
||||
# 약품명 줄바꿈 처리
|
||||
def wrap_text(text, font, max_width):
|
||||
lines = []
|
||||
words = text.split()
|
||||
current_line = ""
|
||||
for word in words:
|
||||
test_line = f"{current_line} {word}".strip()
|
||||
bbox = draw.textbbox((0, 0), test_line, font=font)
|
||||
if bbox[2] - bbox[0] <= max_width:
|
||||
current_line = test_line
|
||||
else:
|
||||
if current_line:
|
||||
lines.append(current_line)
|
||||
current_line = word
|
||||
if current_line:
|
||||
lines.append(current_line)
|
||||
return lines if lines else [text]
|
||||
|
||||
y = 15
|
||||
|
||||
# 환자명 (띄어쓰기)
|
||||
spaced_name = " ".join(patient_name) if patient_name else ""
|
||||
y = draw_centered(spaced_name, y, name_font)
|
||||
|
||||
y += 5
|
||||
|
||||
# 약품명 (줄바꿈)
|
||||
# 괄호 앞에서 분리
|
||||
if '(' in med_name:
|
||||
main_name = med_name.split('(')[0].strip()
|
||||
sub_info = '(' + med_name.split('(', 1)[1] if '(' in med_name else ''
|
||||
else:
|
||||
main_name = med_name
|
||||
sub_info = ''
|
||||
|
||||
# 약품명 줄바꿈
|
||||
name_lines = wrap_text(main_name, drug_font, label_width - 30)
|
||||
for line in name_lines:
|
||||
y = draw_centered(line, y, drug_font)
|
||||
|
||||
# 부가정보
|
||||
if sub_info:
|
||||
y = draw_centered(sub_info, y, small_font, fill="gray")
|
||||
|
||||
y += 5
|
||||
|
||||
# 총량 계산
|
||||
if dosage > 0 and frequency > 0 and duration > 0:
|
||||
total = dosage * frequency * duration
|
||||
total_str = str(int(total)) if total == int(total) else f"{total:.1f}"
|
||||
total_text = f"총 {total_str}{unit} / {duration}일분"
|
||||
y = draw_centered(total_text, y, info_font)
|
||||
|
||||
y += 5
|
||||
|
||||
# 용법 박스
|
||||
box_margin = 20
|
||||
box_top = y
|
||||
box_bottom = y + 70
|
||||
draw.rectangle(
|
||||
[(box_margin, box_top), (label_width - box_margin, box_bottom)],
|
||||
outline="black",
|
||||
width=2
|
||||
)
|
||||
|
||||
# 박스 내용
|
||||
dosage_str = str(int(dosage)) if dosage == int(dosage) else f"{dosage:.2f}".rstrip('0').rstrip('.')
|
||||
dosage_text = f"{dosage_str}{unit}"
|
||||
|
||||
# 복용 시간
|
||||
if frequency == 1:
|
||||
time_text = "아침"
|
||||
elif frequency == 2:
|
||||
time_text = "아침, 저녁"
|
||||
elif frequency == 3:
|
||||
time_text = "아침, 점심, 저녁"
|
||||
else:
|
||||
time_text = f"1일 {frequency}회"
|
||||
|
||||
box_center_y = (box_top + box_bottom) // 2
|
||||
draw_centered(dosage_text, box_center_y - 20, info_font)
|
||||
draw_centered(time_text, box_center_y + 5, info_font)
|
||||
|
||||
y = box_bottom + 10
|
||||
|
||||
# 조제일
|
||||
today = datetime.now().strftime('%Y-%m-%d')
|
||||
y = draw_centered(f"조제일: {today}", y, small_font)
|
||||
|
||||
# 약국명 (하단)
|
||||
pharmacy_y = label_height - 40
|
||||
draw.rectangle(
|
||||
[(50, pharmacy_y - 5), (label_width - 50, pharmacy_y + 25)],
|
||||
outline="black",
|
||||
width=1
|
||||
)
|
||||
draw_centered("청 춘 약 국", pharmacy_y, info_font)
|
||||
|
||||
return image
|
||||
|
||||
@ -295,8 +295,21 @@
|
||||
</div>
|
||||
<div class="action-bar" id="actionBar" style="display:none;">
|
||||
<button class="btn btn-secondary" onclick="selectAll()">전체 선택</button>
|
||||
<button class="btn btn-secondary" onclick="previewLabels()" style="background:#3b82f6;color:#fff;">👁️ 미리보기</button>
|
||||
<button class="btn btn-primary" onclick="printLabels()">🖨️ 라벨 인쇄</button>
|
||||
</div>
|
||||
|
||||
<!-- 미리보기 모달 -->
|
||||
<div id="previewModal" style="display:none;position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,0.7);z-index:1000;overflow-y:auto;">
|
||||
<div style="max-width:400px;margin:50px auto;background:#fff;border-radius:12px;padding:20px;">
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:15px;">
|
||||
<h3 style="margin:0;color:#4c1d95;">🏷️ 라벨 미리보기</h3>
|
||||
<button onclick="closePreview()" style="background:none;border:none;font-size:1.5rem;cursor:pointer;">×</button>
|
||||
</div>
|
||||
<div id="previewContent" style="display:flex;flex-direction:column;gap:15px;align-items:center;">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -466,6 +479,65 @@
|
||||
if (checkAll) checkAll.checked = true;
|
||||
}
|
||||
|
||||
// 라벨 미리보기
|
||||
async function previewLabels() {
|
||||
const rows = document.querySelectorAll('.med-check:checked');
|
||||
if (rows.length === 0) {
|
||||
alert('미리보기할 약품을 선택하세요');
|
||||
return;
|
||||
}
|
||||
|
||||
const container = document.getElementById('previewContent');
|
||||
container.innerHTML = '<div class="loading"><div class="spinner"></div></div>';
|
||||
document.getElementById('previewModal').style.display = 'block';
|
||||
|
||||
// 현재 선택된 환자명
|
||||
const patientName = document.getElementById('detailName').textContent;
|
||||
|
||||
container.innerHTML = '';
|
||||
|
||||
for (const checkbox of rows) {
|
||||
const tr = checkbox.closest('tr');
|
||||
const medName = tr.querySelector('.med-name')?.textContent || '';
|
||||
const dosage = parseFloat(tr.cells[3]?.querySelector('.med-dosage')?.textContent) || 0;
|
||||
const frequency = parseInt(tr.cells[4]?.textContent) || 0;
|
||||
const duration = parseInt(tr.cells[5]?.textContent) || 0;
|
||||
|
||||
try {
|
||||
const res = await fetch('/pmr/api/label/preview', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({
|
||||
patient_name: patientName,
|
||||
med_name: medName,
|
||||
dosage: dosage,
|
||||
frequency: frequency,
|
||||
duration: duration,
|
||||
unit: '정'
|
||||
})
|
||||
});
|
||||
const data = await res.json();
|
||||
|
||||
if (data.success) {
|
||||
const img = document.createElement('img');
|
||||
img.src = data.image;
|
||||
img.style.cssText = 'max-width:100%;border:1px solid #ddd;border-radius:8px;';
|
||||
container.appendChild(img);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Preview error:', err);
|
||||
}
|
||||
}
|
||||
|
||||
if (container.children.length === 0) {
|
||||
container.innerHTML = '<p style="color:#999;">미리보기 생성 실패</p>';
|
||||
}
|
||||
}
|
||||
|
||||
function closePreview() {
|
||||
document.getElementById('previewModal').style.display = 'none';
|
||||
}
|
||||
|
||||
// 라벨 인쇄 (TODO: 구현)
|
||||
function printLabels() {
|
||||
const selected = Array.from(document.querySelectorAll('.med-check:checked')).map(c => c.dataset.code);
|
||||
|
||||
Loading…
Reference in New Issue
Block a user