From 688bdb40f2092e5c9048e9f18f418cf4da7d4a01 Mon Sep 17 00:00:00 2001 From: thug0bin Date: Wed, 11 Mar 2026 16:50:48 +0900 Subject: [PATCH] =?UTF-8?q?feat(admin):=20=EC=A0=9C=ED=92=88=20=EC=82=AC?= =?UTF-8?q?=EC=9A=A9=EC=9D=B4=EB=A0=A5=20+=20=ED=99=98=EC=9E=90=20?= =?UTF-8?q?=EC=B5=9C=EA=B7=BC=EC=B2=98=EB=B0=A9=20=EB=AA=A8=EB=8B=AC=20?= =?UTF-8?q?=EA=B8=B0=EB=8A=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - GET /api/products//usage-history - 제품별 처방 이력 조회 (환자명, 수량, 횟수, 일수, 총투약량) - 페이지네이션 + 기간 필터 지원 - GET /api/patients//recent-prescriptions - 환자 최근 6개월 처방 내역 - 약품별 분류(CD_MC.PRINT_TYPE) 표시 (당뇨치료, 고지혈치료 등) - admin_products.html 모달 2개 추가 - 제품명 클릭 → 사용이력 모달 (횟수 포함 정확한 총투약량) - 환자명 클릭 → 최근처방 모달 (z-index 2100으로 위에 표시) DB 조인: - PS_main.PreSerial = PS_sub_pharm.PreSerial - CD_GOODS + CD_MC (PRINT_TYPE 분류) --- backend/app.py | 232 ++++++++++++++++++++++ backend/templates/admin_products.html | 271 +++++++++++++++++++++++++- 2 files changed, 502 insertions(+), 1 deletion(-) diff --git a/backend/app.py b/backend/app.py index c105861..4a54436 100644 --- a/backend/app.py +++ b/backend/app.py @@ -3872,6 +3872,238 @@ def api_drug_purchase_history(drug_code): return jsonify({'success': False, 'error': str(e)}), 500 +# ==================== 처방 사용이력 API ==================== + +@app.route('/api/products//usage-history') +def api_product_usage_history(drug_code): + """ + 제품 처방 사용이력 조회 API + - PS_main + PS_sub_pharm JOIN + - 페이지네이션, 기간 필터 지원 + - 환자명 마스킹 처리 + """ + page = int(request.args.get('page', 1)) + per_page = int(request.args.get('per_page', 20)) + months = int(request.args.get('months', 12)) + + offset = (page - 1) * per_page + + try: + pres_session = db_manager.get_session('PM_PRES') + + # 기간 계산 (N개월 전부터) + from datetime import datetime, timedelta + start_date = (datetime.now() - timedelta(days=months * 30)).strftime('%Y%m%d') + + # 총 건수 조회 (COUNT) + count_result = pres_session.execute(text(""" + SELECT COUNT(*) as total_count + FROM PS_sub_pharm sp + JOIN PS_main pm ON pm.PreSerial = sp.PreSerial + WHERE sp.DrugCode = :drug_code + AND pm.Indate >= :start_date + AND (sp.PS_Type IS NULL OR sp.PS_Type != '9') + """), {'drug_code': drug_code, 'start_date': start_date}) + total_count = count_result.fetchone()[0] + + # 데이터 조회 (페이지네이션) + data_result = pres_session.execute(text(""" + SELECT + pm.Paname as patient_name, + pm.CusCode as cus_code, + pm.Indate as rx_date, + sp.QUAN as quantity, + sp.QUAN_TIME as times, + sp.Days as days + FROM PS_sub_pharm sp + JOIN PS_main pm ON pm.PreSerial = sp.PreSerial + WHERE sp.DrugCode = :drug_code + AND pm.Indate >= :start_date + AND (sp.PS_Type IS NULL OR sp.PS_Type != '9') + ORDER BY pm.Indate DESC + OFFSET :offset ROWS + FETCH NEXT :per_page ROWS ONLY + """), { + 'drug_code': drug_code, + 'start_date': start_date, + 'offset': offset, + 'per_page': per_page + }) + + items = [] + for row in data_result.fetchall(): + patient_name = row.patient_name or '' + cus_code = row.cus_code or '' + + # 날짜 포맷팅 (YYYYMMDD -> YYYY-MM-DD) + rx_date = row.rx_date or '' + if len(rx_date) == 8: + rx_date = f"{rx_date[:4]}-{rx_date[4:6]}-{rx_date[6:8]}" + + quantity = int(row.quantity) if row.quantity else 1 + times = int(row.times) if row.times else 1 # 횟수 (QUAN_TIME) + days = int(row.days) if row.days else 1 + total_dose = quantity * times * days # 수량 × 횟수 × 일수 + + items.append({ + 'patient_name': patient_name, + 'cus_code': cus_code, + 'rx_date': rx_date, + 'quantity': quantity, + 'times': times, + 'days': days, + 'total_dose': total_dose + }) + + # 약품명 조회 + drug_session = db_manager.get_session('PM_DRUG') + name_result = drug_session.execute(text(""" + SELECT GoodsName FROM CD_GOODS WHERE DrugCode = :drug_code + """), {'drug_code': drug_code}) + name_row = name_result.fetchone() + product_name = name_row[0] if name_row else drug_code + + # 총 페이지 수 계산 + total_pages = (total_count + per_page - 1) // per_page + + return jsonify({ + 'success': True, + 'product_name': product_name, + 'pagination': { + 'page': page, + 'per_page': per_page, + 'total_count': total_count, + 'total_pages': total_pages + }, + 'items': items + }) + + except Exception as e: + logging.error(f"사용이력 조회 오류: {e}") + return jsonify({'success': False, 'error': str(e)}), 500 + + +@app.route('/api/patients//recent-prescriptions') +def api_patient_recent_prescriptions(cus_code): + """ + 환자 최근 처방 내역 조회 API + - 해당 환자가 최근에 어떤 약을 처방받았는지 확인 + - DB 부담 최소화: 최근 6개월, 최대 30건 + """ + try: + pres_session = db_manager.get_session('PM_PRES') + drug_session = db_manager.get_session('PM_DRUG') + + # 최근 6개월 + from datetime import datetime, timedelta + start_date = (datetime.now() - timedelta(days=180)).strftime('%Y%m%d') + + # 환자의 최근 처방전 조회 (최대 10건) + rx_result = pres_session.execute(text(""" + SELECT TOP 10 + pm.PreSerial, + pm.Paname as patient_name, + pm.Indate as rx_date, + pm.Drname as doctor_name, + pm.OrderName as hospital_name + FROM PS_main pm + WHERE pm.CusCode = :cus_code + AND pm.Indate >= :start_date + ORDER BY pm.Indate DESC + """), {'cus_code': cus_code, 'start_date': start_date}) + + prescriptions = [] + for rx in rx_result.fetchall(): + # 날짜 포맷팅 + rx_date = rx.rx_date or '' + if len(rx_date) == 8: + rx_date = f"{rx_date[:4]}-{rx_date[4:6]}-{rx_date[6:8]}" + + # 해당 처방의 약품 목록 조회 + items_result = pres_session.execute(text(""" + SELECT + sp.DrugCode, + sp.QUAN as quantity, + sp.QUAN_TIME as times, + sp.Days as days + FROM PS_sub_pharm sp + WHERE sp.PreSerial = :pre_serial + AND (sp.PS_Type IS NULL OR sp.PS_Type != '9') + """), {'pre_serial': rx.PreSerial}) + + # 먼저 모든 약품 데이터를 리스트로 가져오기 + raw_items = items_result.fetchall() + drug_codes = [item.DrugCode for item in raw_items] + + # 약품명 + 성분명 + 분류(PRINT_TYPE) 일괄 조회 + drug_info_map = {} + if drug_codes: + placeholders = ','.join([f"'{dc}'" for dc in drug_codes]) + name_result = drug_session.execute(text(f""" + SELECT g.DrugCode, g.GoodsName, s.SUNG_HNM, m.PRINT_TYPE + FROM CD_GOODS g + LEFT JOIN CD_SUNG s ON g.SUNG_CODE = s.SUNG_CODE + LEFT JOIN CD_MC m ON g.DrugCode = m.DRUGCODE + WHERE g.DrugCode IN ({placeholders}) + """)) + for row in name_result.fetchall(): + drug_info_map[row[0]] = { + 'name': row[1], + 'ingredient': row[2] or '', + 'category': row[3] or '' # 분류 (알러지질환약 등) + } + + items = [] + for item in raw_items: + info = drug_info_map.get(item.DrugCode, {}) + drug_name = info.get('name', item.DrugCode) + ingredient = info.get('ingredient', '') + category = info.get('category', '') # 분류 (알러지질환약 등) + + quantity = int(item.quantity) if item.quantity else 1 + times = int(item.times) if item.times else 1 + days = int(item.days) if item.days else 1 + + items.append({ + 'drug_code': item.DrugCode, + 'drug_name': drug_name, + 'category': category, # 분류 추가 + 'quantity': quantity, + 'times': times, + 'days': days, + 'total_dose': quantity * times * days + }) + + prescriptions.append({ + 'pre_serial': rx.PreSerial, + 'rx_date': rx_date, + 'doctor_name': rx.doctor_name or '', + 'hospital_name': rx.hospital_name or '', + 'items': items + }) + + # 환자명 + patient_name = prescriptions[0]['items'][0]['drug_name'] if prescriptions else '' + if prescriptions and pres_session.execute(text(""" + SELECT TOP 1 Paname FROM PS_main WHERE CusCode = :cus_code + """), {'cus_code': cus_code}).fetchone(): + patient_name = pres_session.execute(text(""" + SELECT TOP 1 Paname FROM PS_main WHERE CusCode = :cus_code + """), {'cus_code': cus_code}).fetchone()[0] + + return jsonify({ + 'success': True, + 'cus_code': cus_code, + 'patient_name': patient_name, + 'prescription_count': len(prescriptions), + 'prescriptions': prescriptions + }) + + except Exception as e: + logging.error(f"환자 처방 조회 오류: {e}") + return jsonify({'success': False, 'error': str(e)}), 500 + + # ==================== 위치 정보 API ==================== @app.route('/api/locations') diff --git a/backend/templates/admin_products.html b/backend/templates/admin_products.html index a177012..81e589b 100644 --- a/backend/templates/admin_products.html +++ b/backend/templates/admin_products.html @@ -712,6 +712,57 @@ .purchase-modal-btn:hover { background: #e2e8f0; } + + /* ── 제품명 링크 스타일 ── */ + .product-name-link { + cursor: pointer; + transition: color 0.15s; + } + .product-name-link:hover { + color: #8b5cf6; + text-decoration: underline; + } + + /* ── 환자명 링크 스타일 ── */ + .patient-name-link { + cursor: pointer; + color: #1e293b; + transition: all 0.15s; + padding: 2px 6px; + border-radius: 4px; + } + .patient-name-link:hover { + color: #8b5cf6; + background: #f3e8ff; + text-decoration: underline; + } + + /* ── 페이지네이션 버튼 ── */ + .pagination-btn { + padding: 6px 12px; + border: 1px solid #e2e8f0; + border-radius: 6px; + background: #fff; + color: #475569; + cursor: pointer; + font-size: 13px; + font-weight: 500; + transition: all 0.15s; + } + .pagination-btn:hover:not(:disabled) { + background: #f1f5f9; + border-color: #cbd5e1; + } + .pagination-btn.active { + background: #10b981; + color: #fff; + border-color: #10b981; + } + .pagination-btn:disabled { + opacity: 0.5; + cursor: not-allowed; + } + tbody tr { cursor: pointer; } @@ -1151,6 +1202,53 @@ + + +
+
+
+

📋 환자 최근 처방

+
-
+
+
+
📭

로딩 중...

+
+ +
+
+ +
+
+
+

💊 처방 사용이력

+
-
+
+
+ + + + + + + + + + + + + + +
환자명처방일수량횟수일수총투약량
📭

로딩 중...

+
+ +
+
+