- /pmr/api/patient/<cus_code>/history: 환자 이전 처방 이력 API - 하단에 이전 처방 영역 + < > 네비게이션 - 현재 처방과 이전 처방 한눈에 비교 가능
631 lines
22 KiB
Python
631 lines
22 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
|
|
|
|
|
|
# ─────────────────────────────────────────────────────────────
|
|
# API: 환자 이전 처방 이력
|
|
# ─────────────────────────────────────────────────────────────
|
|
@pmr_bp.route('/api/patient/<cus_code>/history', methods=['GET'])
|
|
def get_patient_history(cus_code):
|
|
"""
|
|
환자 이전 처방 이력 조회
|
|
|
|
Args:
|
|
cus_code: 환자 고유코드 (CusCode)
|
|
|
|
Query Params:
|
|
- limit: 최대 조회 건수 (기본 10, 최대 50)
|
|
- exclude: 제외할 처방번호 (현재 처방)
|
|
"""
|
|
try:
|
|
limit = min(int(request.args.get('limit', 10)), 50)
|
|
exclude_serial = request.args.get('exclude', '')
|
|
|
|
conn = get_mssql_connection('PM_PRES')
|
|
cursor = conn.cursor()
|
|
|
|
# 이전 처방 목록 조회
|
|
query = """
|
|
SELECT TOP (?)
|
|
m.PreSerial,
|
|
m.PassDay,
|
|
m.PresTime,
|
|
m.Paname,
|
|
m.InsName,
|
|
m.Drname,
|
|
m.PRICE_P,
|
|
(SELECT COUNT(*) FROM PS_sub_pharm WHERE PreSerial = m.PreSerial) as drug_count
|
|
FROM PS_MAIN m
|
|
WHERE m.CusCode = ?
|
|
"""
|
|
params = [limit, cus_code]
|
|
|
|
if exclude_serial:
|
|
query += " AND m.PreSerial != ?"
|
|
params.append(exclude_serial)
|
|
|
|
query += " ORDER BY m.PassDay DESC, m.PresTime DESC"
|
|
|
|
cursor.execute(query, params)
|
|
|
|
history = []
|
|
for row in cursor.fetchall():
|
|
pre_serial = row.PreSerial
|
|
|
|
# 해당 처방의 약품 목록 조회
|
|
cursor.execute("""
|
|
SELECT
|
|
s.DrugCode,
|
|
s.Days,
|
|
s.QUAN,
|
|
s.QUAN_TIME,
|
|
g.GoodsName,
|
|
m.PRINT_TYPE
|
|
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
|
|
""", (pre_serial,))
|
|
|
|
medications = []
|
|
for med_row in cursor.fetchall():
|
|
medications.append({
|
|
'medication_code': med_row.DrugCode or '',
|
|
'med_name': med_row.GoodsName or med_row.DrugCode or '',
|
|
'add_info': med_row.PRINT_TYPE or '',
|
|
'dosage': float(med_row.QUAN) if med_row.QUAN else 0,
|
|
'frequency': med_row.QUAN_TIME or 0,
|
|
'duration': med_row.Days or 0
|
|
})
|
|
|
|
# 날짜 포맷
|
|
pass_day = row.PassDay
|
|
if pass_day and len(pass_day) == 8:
|
|
date_formatted = f"{pass_day[:4]}-{pass_day[4:6]}-{pass_day[6:8]}"
|
|
else:
|
|
date_formatted = pass_day or ''
|
|
|
|
history.append({
|
|
'prescription_id': pre_serial,
|
|
'date': date_formatted,
|
|
'time': row.PresTime or '',
|
|
'patient_name': row.Paname or '',
|
|
'hospital': row.InsName or '',
|
|
'doctor': row.Drname or '',
|
|
'copayment': int(row.PRICE_P or 0),
|
|
'medication_count': row.drug_count or 0,
|
|
'medications': medications
|
|
})
|
|
|
|
conn.close()
|
|
|
|
return jsonify({
|
|
'success': True,
|
|
'cus_code': cus_code,
|
|
'count': len(history),
|
|
'history': history
|
|
})
|
|
|
|
except Exception as e:
|
|
logging.error(f"환자 이전 처방 조회 오류: {e}")
|
|
return jsonify({'success': False, 'error': str(e)}), 500
|