# 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/', methods=['GET']) def get_prescription_detail(prescription_id): """ 처방전 상세 정보 (약품 목록 포함) """ try: conn = get_mssql_connection('PM_PRES') cursor = conn.cursor() # 처방전 기본 정보 + 질병정보 cursor.execute(""" SELECT M.PreSerial, M.Day_Serial, M.PassDay, M.Paname, M.PaNum, M.CusCode, M.InsName, M.Drname, M.PresTime, M.PreGubun, M.PRICE_T, M.PRICE_P, M.PRICE_C, M.St1, M.St2, S1.AU_NAME, S2.AU_NAME FROM PS_MAIN M LEFT JOIN PM_BASE.dbo.CD_SANG S1 ON M.St1 = S1.AU_CODE LEFT JOIN PM_BASE.dbo.CD_SANG S2 ON M.St2 = S2.AU_CODE WHERE M.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 # 질병 정보 (St1, St2 → AU_NAME) disease_info = None st1 = rx_row[13] or '' # St1 st2 = rx_row[14] or '' # St2 disease_name_1 = rx_row[15] or '' # S1.AU_NAME disease_name_2 = rx_row[16] or '' # S2.AU_NAME if st1 or st2: disease_info = { 'code_1': st1, 'code_2': st2, 'name_1': disease_name_1, 'name_2': disease_name_2 } 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 }, 'disease_info': disease_info, '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//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