diff --git a/backend/pmr_api.py b/backend/pmr_api.py index 99e6312..52d1f83 100644 --- a/backend/pmr_api.py +++ b/backend/pmr_api.py @@ -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 diff --git a/backend/templates/pmr.html b/backend/templates/pmr.html index acb0b8d..8e1bfbc 100644 --- a/backend/templates/pmr.html +++ b/backend/templates/pmr.html @@ -295,8 +295,21 @@ + + + @@ -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 = '
'; + 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 = '

미리보기 생성 실패

'; + } + } + + 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);