pharmacy-pos-qr-system/backend/pmr_api.py
thug0bin fc2db78816 feat: PMR 효능효과(add_info) 추가
- CD_MC.PRINT_TYPE JOIN으로 효능 조회
- 약품 테이블에 효능 표시
- 라벨 미리보기에 효능 포함
2026-03-04 22:56:28 +09:00

521 lines
18 KiB
Python

# pmr_api.py - 조제관리(PMR) Blueprint API
# PharmaIT3000 MSSQL 연동 (192.168.0.4)
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')
# ─────────────────────────────────────────────────────────────
# MSSQL 연결 설정 (PharmaIT3000 - 192.168.0.4)
# ─────────────────────────────────────────────────────────────
MSSQL_CONFIG = {
'server': '192.168.0.4\\PM2014',
'username': 'sa',
'password': 'tmddls214!%(',
'driver': 'ODBC Driver 17 for SQL Server'
}
def get_mssql_connection(database='PM_PRES'):
"""MSSQL 연결 획득"""
conn_str = (
f"DRIVER={{{MSSQL_CONFIG['driver']}}};"
f"SERVER={MSSQL_CONFIG['server']};"
f"DATABASE={database};"
f"UID={MSSQL_CONFIG['username']};"
f"PWD={MSSQL_CONFIG['password']};"
"TrustServerCertificate=yes;"
"Connection Timeout=10"
)
return pyodbc.connect(conn_str, timeout=10)
# ─────────────────────────────────────────────────────────────
# 조제관리 페이지
# ─────────────────────────────────────────────────────────────
@pmr_bp.route('/')
def pmr_index():
"""조제관리 메인 페이지"""
return render_template('pmr.html')
# ─────────────────────────────────────────────────────────────
# API: 날짜별 처방전 목록
# ─────────────────────────────────────────────────────────────
@pmr_bp.route('/api/prescriptions', methods=['GET'])
def get_prescriptions_by_date():
"""
날짜별 처방전 목록 조회
Query Params:
- date: YYYY-MM-DD (기본값: 오늘)
"""
try:
date_str = request.args.get('date', date.today().strftime('%Y-%m-%d'))
# YYYYMMDD 형식으로 변환
date_yyyymmdd = date_str.replace('-', '')
conn = get_mssql_connection('PM_PRES')
cursor = conn.cursor()
cursor.execute("""
SELECT
PreSerial,
Day_Serial,
PassDay,
Paname,
PaNum,
CusCode,
InsName,
Drname,
PresTime,
PreGubun,
PRICE_T,
PRICE_P,
PRICE_C
FROM PS_MAIN
WHERE PassDay = ?
ORDER BY Day_Serial ASC
""", (date_yyyymmdd,))
prescriptions = []
for row in cursor.fetchall():
# 주민번호에서 나이/성별 추출
panum = row.PaNum or ''
age = None
gender = None
if len(panum) >= 7:
try:
birth_year = int(panum[:2])
gender_code = panum[6] if len(panum) > 6 else ''
# 성별 및 세기 판단
if gender_code in ['1', '2', '5', '6']:
birth_year += 1900
elif gender_code in ['3', '4', '7', '8']:
birth_year += 2000
else:
birth_year += 1900
gender = '' if gender_code in ['1', '3', '5', '7'] else ''
age = datetime.now().year - birth_year
except:
pass
prescriptions.append({
'prescription_id': row.PreSerial,
'order_number': row.Day_Serial,
'date': row.PassDay,
'patient_name': row.Paname,
'patient_id': row.PaNum[:6] + '******' if row.PaNum and len(row.PaNum) > 6 else row.PaNum,
'patient_code': row.CusCode,
'hospital': row.InsName,
'doctor': row.Drname,
'time': row.PresTime,
'type': '급여' if row.PreGubun == '0' else '비급여' if row.PreGubun == '9' else row.PreGubun,
'age': age,
'gender': gender,
'price_total': row.PRICE_T,
'price_patient': row.PRICE_P,
'price_claim': row.PRICE_C
})
conn.close()
return jsonify({
'success': True,
'date': date_str,
'count': len(prescriptions),
'prescriptions': prescriptions
})
except Exception as e:
logging.error(f"PMR 처방전 목록 조회 오류: {e}")
return jsonify({'success': False, 'error': str(e)}), 500
# ─────────────────────────────────────────────────────────────
# API: 처방전 상세 (약품 목록)
# ─────────────────────────────────────────────────────────────
@pmr_bp.route('/api/prescription/<prescription_id>', methods=['GET'])
def get_prescription_detail(prescription_id):
"""
처방전 상세 정보 (약품 목록 포함)
"""
try:
conn = get_mssql_connection('PM_PRES')
cursor = conn.cursor()
# 처방전 기본 정보
cursor.execute("""
SELECT
PreSerial,
Day_Serial,
PassDay,
Paname,
PaNum,
CusCode,
InsName,
Drname,
PresTime,
PreGubun,
PRICE_T,
PRICE_P,
PRICE_C
FROM PS_MAIN
WHERE PreSerial = ?
""", (prescription_id,))
rx_row = cursor.fetchone()
if not rx_row:
conn.close()
return jsonify({'success': False, 'error': '처방전을 찾을 수 없습니다'}), 404
# 처방 약품 목록 (PS_sub_pharm + CD_GOODS + CD_MC JOIN)
medications = []
cursor.execute("""
SELECT
s.DrugCode,
s.Days,
s.QUAN,
s.QUAN_TIME,
s.PS_Type,
s.INV_QUAN,
g.GoodsName,
g.SUNG_CODE,
m.PRINT_TYPE,
m.SIM_EFFECT
FROM PS_sub_pharm s
LEFT JOIN PM_DRUG.dbo.CD_GOODS g ON s.DrugCode = g.DrugCode
LEFT JOIN PM_DRUG.dbo.CD_MC m ON s.DrugCode = m.DRUGCODE
WHERE s.PreSerial = ?
ORDER BY s.SUB_SERIAL
""", (prescription_id,))
for row in cursor.fetchall():
# 효능: PRINT_TYPE > SIM_EFFECT > 없음
add_info = row.PRINT_TYPE or row.SIM_EFFECT or ''
medications.append({
'medication_code': row.DrugCode or '',
'med_name': row.GoodsName or row.DrugCode or '',
'add_info': add_info,
'dosage': float(row.QUAN) if row.QUAN else 0,
'frequency': row.QUAN_TIME or 0,
'duration': row.Days or 0,
'total_qty': float(row.INV_QUAN) if row.INV_QUAN else 0,
'type': '급여' if row.PS_Type in ['0', '4'] else '비급여' if row.PS_Type == '1' else row.PS_Type,
'sung_code': row.SUNG_CODE or ''
})
conn.close()
# 나이/성별 계산
panum = rx_row.PaNum or ''
age = None
gender = None
if len(panum) >= 7:
try:
birth_year = int(panum[:2])
gender_code = panum[6]
if gender_code in ['1', '2', '5', '6']:
birth_year += 1900
elif gender_code in ['3', '4', '7', '8']:
birth_year += 2000
gender = '' if gender_code in ['1', '3', '5', '7'] else ''
age = datetime.now().year - birth_year
except:
pass
return jsonify({
'success': True,
'prescription': {
'prescription_id': rx_row.PreSerial,
'order_number': rx_row.Day_Serial,
'date': rx_row.PassDay,
'time': rx_row.PresTime,
'hospital': rx_row.InsName,
'doctor': rx_row.Drname,
'type': '급여' if rx_row.PreGubun == '0' else '비급여',
'price_total': rx_row.PRICE_T,
'price_patient': rx_row.PRICE_P
},
'patient': {
'name': rx_row.Paname,
'code': rx_row.CusCode,
'age': age,
'gender': gender
},
'medications': medications,
'medication_count': len(medications)
})
except Exception as e:
logging.error(f"PMR 처방전 상세 조회 오류: {e}")
return jsonify({'success': False, 'error': str(e)}), 500
# ─────────────────────────────────────────────────────────────
# API: 통계 (당일 요약)
# ─────────────────────────────────────────────────────────────
@pmr_bp.route('/api/stats', methods=['GET'])
def get_daily_stats():
"""당일 조제 통계"""
try:
date_str = request.args.get('date', date.today().strftime('%Y-%m-%d'))
date_yyyymmdd = date_str.replace('-', '')
conn = get_mssql_connection('PM_PRES')
cursor = conn.cursor()
# 처방전 수
cursor.execute("""
SELECT COUNT(*) as cnt
FROM PS_MAIN
WHERE PassDay = ?
""", (date_yyyymmdd,))
total_prescriptions = cursor.fetchone()[0]
# 총 금액
cursor.execute("""
SELECT
ISNULL(SUM(PRICE_T), 0) as total,
ISNULL(SUM(PRICE_P), 0) as patient,
ISNULL(SUM(PRICE_C), 0) as claim
FROM PS_MAIN
WHERE PassDay = ?
""", (date_yyyymmdd,))
price_row = cursor.fetchone()
conn.close()
return jsonify({
'success': True,
'date': date_str,
'stats': {
'total_prescriptions': total_prescriptions,
'total_amount': price_row[0] if price_row else 0,
'patient_amount': price_row[1] if price_row else 0,
'claim_amount': price_row[2] if price_row else 0
}
})
except Exception as e:
logging.error(f"PMR 통계 조회 오류: {e}")
return jsonify({'success': False, 'error': str(e)}), 500
# ─────────────────────────────────────────────────────────────
# API: DB 연결 테스트
# ─────────────────────────────────────────────────────────────
@pmr_bp.route('/api/test', methods=['GET'])
def test_connection():
"""DB 연결 테스트"""
try:
conn = get_mssql_connection('PM_PRES')
cursor = conn.cursor()
cursor.execute("SELECT @@VERSION")
version = cursor.fetchone()[0]
conn.close()
return jsonify({
'success': True,
'server': MSSQL_CONFIG['server'],
'version': version[:100] + '...'
})
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', '')
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', '')
# 라벨 이미지 생성
image = create_label_image(
patient_name=patient_name,
med_name=med_name,
add_info=add_info,
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, add_info='', dosage=0, frequency=0, duration=0, 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()
else:
main_name = med_name
# 약품명 줄바꿈
name_lines = wrap_text(main_name, drug_font, label_width - 30)
for line in name_lines:
y = draw_centered(line, y, drug_font)
# 효능효과 (add_info)
if add_info:
y = draw_centered(f"({add_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