feat(drug-usage): 단위 마스터 + 총사용량 표시 + 순차 API 호출
- drug_unit.py: SUNG_CODE 기반 단위 판별 함수 추가 - 조제 상세에 총사용량 + 단위 표시 (예: 1,230정) - API 순차 호출로 DB 세션 충돌 방지
This commit is contained in:
parent
91f36273e9
commit
80b3919ac9
@ -8905,6 +8905,8 @@ def api_drug_usage_imports(drug_code):
|
|||||||
@app.route('/api/drug-usage/<drug_code>/prescriptions')
|
@app.route('/api/drug-usage/<drug_code>/prescriptions')
|
||||||
def api_drug_usage_prescriptions(drug_code):
|
def api_drug_usage_prescriptions(drug_code):
|
||||||
"""약품별 조제(매출) 상세 API"""
|
"""약품별 조제(매출) 상세 API"""
|
||||||
|
from utils.drug_unit import get_drug_unit
|
||||||
|
|
||||||
start_date = request.args.get('start_date', '')
|
start_date = request.args.get('start_date', '')
|
||||||
end_date = request.args.get('end_date', '')
|
end_date = request.args.get('end_date', '')
|
||||||
|
|
||||||
@ -8913,6 +8915,16 @@ def api_drug_usage_prescriptions(drug_code):
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
pres_session = db_manager.get_session('PM_PRES')
|
pres_session = db_manager.get_session('PM_PRES')
|
||||||
|
drug_session = db_manager.get_session('PM_DRUG')
|
||||||
|
|
||||||
|
# 약품 정보 조회 (단위 판별용)
|
||||||
|
drug_info = drug_session.execute(text("""
|
||||||
|
SELECT GoodsName, SUNG_CODE FROM CD_GOODS WHERE DrugCode = :drug_code
|
||||||
|
"""), {'drug_code': drug_code}).fetchone()
|
||||||
|
|
||||||
|
goods_name = drug_info.GoodsName if drug_info else ''
|
||||||
|
sung_code = drug_info.SUNG_CODE if drug_info else ''
|
||||||
|
unit = get_drug_unit(goods_name, sung_code)
|
||||||
|
|
||||||
result = pres_session.execute(text("""
|
result = pres_session.execute(text("""
|
||||||
SELECT
|
SELECT
|
||||||
@ -8934,12 +8946,15 @@ def api_drug_usage_prescriptions(drug_code):
|
|||||||
items = []
|
items = []
|
||||||
seen_patients = set()
|
seen_patients = set()
|
||||||
recent_patients = [] # 최근 조제받은 환자 (중복 제외, 최대 3명)
|
recent_patients = [] # 최근 조제받은 환자 (중복 제외, 최대 3명)
|
||||||
|
total_usage = 0 # 총 사용량
|
||||||
|
|
||||||
for row in result:
|
for row in result:
|
||||||
dosage = float(row.dosage) if row.dosage else 0
|
dosage = float(row.dosage) if row.dosage else 0
|
||||||
freq = float(row.frequency) if row.frequency else 0
|
freq = float(row.frequency) if row.frequency else 0
|
||||||
days = int(row.days) if row.days else 0
|
days = int(row.days) if row.days else 0
|
||||||
patient = row.patient_name or ''
|
patient = row.patient_name or ''
|
||||||
|
qty = dosage * freq * days
|
||||||
|
total_usage += qty
|
||||||
|
|
||||||
# 중복 제외 환자 목록 (최근순, 최대 3명)
|
# 중복 제외 환자 목록 (최근순, 최대 3명)
|
||||||
if patient and patient not in seen_patients:
|
if patient and patient not in seen_patients:
|
||||||
@ -8955,7 +8970,7 @@ def api_drug_usage_prescriptions(drug_code):
|
|||||||
'dosage': dosage,
|
'dosage': dosage,
|
||||||
'frequency': freq,
|
'frequency': freq,
|
||||||
'days': days,
|
'days': days,
|
||||||
'total_qty': dosage * freq * days
|
'total_qty': qty
|
||||||
})
|
})
|
||||||
|
|
||||||
return jsonify({
|
return jsonify({
|
||||||
@ -8964,6 +8979,8 @@ def api_drug_usage_prescriptions(drug_code):
|
|||||||
'total_count': len(items),
|
'total_count': len(items),
|
||||||
'unique_patients': len(seen_patients),
|
'unique_patients': len(seen_patients),
|
||||||
'recent_patients': recent_patients,
|
'recent_patients': recent_patients,
|
||||||
|
'total_usage': total_usage,
|
||||||
|
'unit': unit,
|
||||||
'items': items
|
'items': items
|
||||||
})
|
})
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|||||||
@ -392,6 +392,16 @@
|
|||||||
background: #fef3c7;
|
background: #fef3c7;
|
||||||
color: #92400e;
|
color: #92400e;
|
||||||
}
|
}
|
||||||
|
.usage-badge {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 2px 10px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
background: linear-gradient(135deg, #10b981, #059669);
|
||||||
|
color: #fff;
|
||||||
|
border-radius: 12px;
|
||||||
|
margin-left: 8px;
|
||||||
|
}
|
||||||
.detail-table-wrapper {
|
.detail-table-wrapper {
|
||||||
max-height: 300px;
|
max-height: 300px;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
@ -805,14 +815,13 @@
|
|||||||
mainRow.classList.add('expanded');
|
mainRow.classList.add('expanded');
|
||||||
expandedDrugCode = drugCode;
|
expandedDrugCode = drugCode;
|
||||||
|
|
||||||
// 데이터 로드
|
// 데이터 로드 (순차 호출 - DB 세션 충돌 방지)
|
||||||
const startDate = document.getElementById('startDate').value.replace(/-/g, '');
|
const startDate = document.getElementById('startDate').value.replace(/-/g, '');
|
||||||
const endDate = document.getElementById('endDate').value.replace(/-/g, '');
|
const endDate = document.getElementById('endDate').value.replace(/-/g, '');
|
||||||
|
|
||||||
// 입고 데이터 로드
|
// 입고 데이터 로드 후 조제 데이터 로드 (순차)
|
||||||
loadImports(drugCode, startDate, endDate);
|
await loadImports(drugCode, startDate, endDate);
|
||||||
// 조제 데이터 로드
|
await loadPrescriptions(drugCode, startDate, endDate);
|
||||||
loadPrescriptions(drugCode, startDate, endDate);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ═══ 입고 데이터 로드 ═══
|
// ═══ 입고 데이터 로드 ═══
|
||||||
@ -901,8 +910,13 @@
|
|||||||
).join('') + `<span class="patient-badge more">외 ${uniqueCount - 3}명</span>`;
|
).join('') + `<span class="patient-badge more">외 ${uniqueCount - 3}명</span>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 총 사용량 + 단위
|
||||||
|
const totalUsage = data.total_usage || 0;
|
||||||
|
const unit = data.unit || '개';
|
||||||
|
const usageBadge = `<span class="usage-badge">${formatNumber(totalUsage)}${unit}</span>`;
|
||||||
|
|
||||||
container.innerHTML = `
|
container.innerHTML = `
|
||||||
<h4>💊 조제목록 <span class="count">(${data.total_count}건)</span> <span class="patient-info">${patientBadges}</span></h4>
|
<h4>💊 조제목록 <span class="count">(${data.total_count}건)</span> ${usageBadge} <span class="patient-info">${patientBadges}</span></h4>
|
||||||
<div class="detail-table-wrapper">
|
<div class="detail-table-wrapper">
|
||||||
<table class="detail-table" style="table-layout:fixed;">
|
<table class="detail-table" style="table-layout:fixed;">
|
||||||
<colgroup>
|
<colgroup>
|
||||||
|
|||||||
160
backend/utils/drug_unit.py
Normal file
160
backend/utils/drug_unit.py
Normal file
@ -0,0 +1,160 @@
|
|||||||
|
"""
|
||||||
|
약품 포장단위 판별 유틸리티
|
||||||
|
SUNG_CODE 기반으로 약품의 단위(정, 캡슐, mL, 포 등)를 판별
|
||||||
|
|
||||||
|
참고: person-lookup-web-local/dev_docs/pharmit_3000db_sung_code.md
|
||||||
|
"""
|
||||||
|
import re
|
||||||
|
|
||||||
|
# FormCode -> 기본 단위 매핑
|
||||||
|
FORM_CODE_UNIT_MAP = {
|
||||||
|
# 정제류
|
||||||
|
'TA': '정', 'TB': '정', 'TC': '정', 'TD': '정', 'TE': '정',
|
||||||
|
'TF': '정', 'TG': '정', 'TH': '정', 'TL': '정', 'TR': '정',
|
||||||
|
|
||||||
|
# 캡슐류
|
||||||
|
'CA': '캡슐', 'CB': '캡슐', 'CC': '캡슐', 'CD': '캡슐', 'CE': '캡슐',
|
||||||
|
'CH': '캡슐', 'CR': '캡슐', 'CS': '캡슐',
|
||||||
|
|
||||||
|
# 과립/산제
|
||||||
|
'GA': '포', 'GB': '포', 'GC': '포', 'GN': '포', 'PD': '포',
|
||||||
|
|
||||||
|
# 액상제
|
||||||
|
'SS': 'mL', 'SY': 'mL', 'LQ': 'mL', 'SI': '앰플',
|
||||||
|
|
||||||
|
# 외용제
|
||||||
|
'EY': '병', 'EN': '병', 'EO': '병', 'OS': '병', 'OO': '튜브',
|
||||||
|
'GT': '포', 'OT': '개', 'OM': '개', 'CT': '개', 'CM': '개',
|
||||||
|
'LT': '개', 'PT': '매', 'PC': '매', 'SP': '병',
|
||||||
|
|
||||||
|
# 좌제/질정
|
||||||
|
'SU': '개', 'VT': '개',
|
||||||
|
|
||||||
|
# 주사제
|
||||||
|
'IN': '바이알', 'IA': '앰플', 'IJ': '바이알', 'IP': '프리필드',
|
||||||
|
|
||||||
|
# 흡입제
|
||||||
|
'IH': '개', 'NE': '앰플',
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def get_drug_unit(goods_name: str, sung_code: str) -> str:
|
||||||
|
"""
|
||||||
|
약품명과 SUNG_CODE를 기반으로 포장단위를 판별
|
||||||
|
|
||||||
|
Args:
|
||||||
|
goods_name: 약품명 (예: "씨투스건조시럽_(0.5g)")
|
||||||
|
sung_code: SUNG_CODE (예: "100701ATB" - 마지막 2자리가 FormCode)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
포장단위 문자열 (예: "정", "캡슐", "mL", "포" 등)
|
||||||
|
"""
|
||||||
|
if not sung_code or len(sung_code) < 2:
|
||||||
|
return '개' # 기본값
|
||||||
|
|
||||||
|
# FormCode 추출 (SUNG_CODE 마지막 2자리)
|
||||||
|
form_code = sung_code[-2:].upper()
|
||||||
|
|
||||||
|
# 건조시럽(SS) / 시럽(SY) 특수 처리
|
||||||
|
if form_code in ('SS', 'SY'):
|
||||||
|
return _get_syrup_unit(goods_name)
|
||||||
|
|
||||||
|
# 점안액(EY, OS) 특수 처리
|
||||||
|
if form_code in ('EY', 'OS'):
|
||||||
|
return _get_eye_drop_unit(goods_name)
|
||||||
|
|
||||||
|
# 안연고(OO) 특수 처리
|
||||||
|
if form_code == 'OO':
|
||||||
|
if '안연고' in goods_name or '눈연고' in goods_name:
|
||||||
|
return '튜브'
|
||||||
|
return '개'
|
||||||
|
|
||||||
|
# 액제(LQ) 특수 처리
|
||||||
|
if form_code == 'LQ':
|
||||||
|
return _get_liquid_unit(goods_name)
|
||||||
|
|
||||||
|
# 파우더/산제(PD, GN) 특수 처리
|
||||||
|
if form_code in ('PD', 'GN'):
|
||||||
|
return _get_powder_unit(goods_name)
|
||||||
|
|
||||||
|
# 흡입제/스프레이(SI) 특수 처리
|
||||||
|
if form_code == 'SI':
|
||||||
|
if '흡입액' in goods_name or '네뷸' in goods_name:
|
||||||
|
return '앰플'
|
||||||
|
return '개'
|
||||||
|
|
||||||
|
# 기본 매핑에서 찾기
|
||||||
|
return FORM_CODE_UNIT_MAP.get(form_code, '개')
|
||||||
|
|
||||||
|
|
||||||
|
def _get_syrup_unit(goods_name: str) -> str:
|
||||||
|
"""시럽/건조시럽 단위 판별"""
|
||||||
|
# 개별 g 포장: (0.5g), (0.7g) 등 -> 포
|
||||||
|
if re.search(r'\([\d.]+g\)', goods_name):
|
||||||
|
return '포'
|
||||||
|
|
||||||
|
# g/Xg 벌크 패턴 -> g
|
||||||
|
if re.search(r'_\([^)]+/\d+g\)', goods_name):
|
||||||
|
return 'g'
|
||||||
|
|
||||||
|
# 건조시럽/현탁용분말 -> mL
|
||||||
|
if '건조시럽' in goods_name or '현탁용분말' in goods_name:
|
||||||
|
return 'mL'
|
||||||
|
|
||||||
|
# 소용량 mL (5~30mL) -> 포
|
||||||
|
match = re.search(r'[_(/](\d+)mL\)', goods_name, re.IGNORECASE)
|
||||||
|
if match:
|
||||||
|
volume = int(match.group(1))
|
||||||
|
if volume <= 30:
|
||||||
|
return '포'
|
||||||
|
|
||||||
|
return 'mL'
|
||||||
|
|
||||||
|
|
||||||
|
def _get_eye_drop_unit(goods_name: str) -> str:
|
||||||
|
"""점안액 단위 판별"""
|
||||||
|
# 소용량 (1mL 이하) = 일회용 -> 개
|
||||||
|
match = re.search(r'[_/\(]([\d.]+)mL\)', goods_name)
|
||||||
|
if match:
|
||||||
|
try:
|
||||||
|
volume = float(match.group(1))
|
||||||
|
if volume <= 1.0:
|
||||||
|
return '개'
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
return '병'
|
||||||
|
|
||||||
|
|
||||||
|
def _get_liquid_unit(goods_name: str) -> str:
|
||||||
|
"""액제 단위 판별"""
|
||||||
|
# 알긴산/거드액 -> 포
|
||||||
|
if '알긴' in goods_name or '거드' in goods_name:
|
||||||
|
return '포'
|
||||||
|
|
||||||
|
# 외용액 -> 병
|
||||||
|
if any(k in goods_name for k in ['외용', '네일', '라카', '베이트', '더마톱', '라미실']):
|
||||||
|
return '병'
|
||||||
|
|
||||||
|
# 점이/점비액 -> 병
|
||||||
|
if '점비' in goods_name or '이용액' in goods_name:
|
||||||
|
return '병'
|
||||||
|
|
||||||
|
# 흡입액 -> 앰플
|
||||||
|
if '흡입' in goods_name or '네뷸' in goods_name:
|
||||||
|
return '앰플'
|
||||||
|
|
||||||
|
return 'mL'
|
||||||
|
|
||||||
|
|
||||||
|
def _get_powder_unit(goods_name: str) -> str:
|
||||||
|
"""파우더/산제 단위 판별"""
|
||||||
|
# 분모 10g 이상 = 벌크 -> g
|
||||||
|
match = re.search(r'_\([^)]+/(\d+(?:\.\d+)?)g\)', goods_name)
|
||||||
|
if match:
|
||||||
|
try:
|
||||||
|
denominator = float(match.group(1))
|
||||||
|
if denominator >= 10:
|
||||||
|
return 'g'
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
return '포'
|
||||||
Loading…
Reference in New Issue
Block a user