From 80b3919ac98cea6300f39242bc2ce4ac5408ceca Mon Sep 17 00:00:00 2001 From: thug0bin Date: Wed, 11 Mar 2026 21:47:53 +0900 Subject: [PATCH] =?UTF-8?q?feat(drug-usage):=20=EB=8B=A8=EC=9C=84=20?= =?UTF-8?q?=EB=A7=88=EC=8A=A4=ED=84=B0=20+=20=EC=B4=9D=EC=82=AC=EC=9A=A9?= =?UTF-8?q?=EB=9F=89=20=ED=91=9C=EC=8B=9C=20+=20=EC=88=9C=EC=B0=A8=20API?= =?UTF-8?q?=20=ED=98=B8=EC=B6=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - drug_unit.py: SUNG_CODE 기반 단위 판별 함수 추가 - 조제 상세에 총사용량 + 단위 표시 (예: 1,230정) - API 순차 호출로 DB 세션 충돌 방지 --- backend/app.py | 19 ++- backend/templates/admin_drug_usage.html | 26 +++- backend/utils/drug_unit.py | 160 ++++++++++++++++++++++++ 3 files changed, 198 insertions(+), 7 deletions(-) create mode 100644 backend/utils/drug_unit.py diff --git a/backend/app.py b/backend/app.py index e53b07e..62014c4 100644 --- a/backend/app.py +++ b/backend/app.py @@ -8905,6 +8905,8 @@ def api_drug_usage_imports(drug_code): @app.route('/api/drug-usage//prescriptions') def api_drug_usage_prescriptions(drug_code): """약품별 조제(매출) 상세 API""" + from utils.drug_unit import get_drug_unit + start_date = request.args.get('start_date', '') end_date = request.args.get('end_date', '') @@ -8913,6 +8915,16 @@ def api_drug_usage_prescriptions(drug_code): try: 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(""" SELECT @@ -8934,12 +8946,15 @@ def api_drug_usage_prescriptions(drug_code): items = [] seen_patients = set() recent_patients = [] # 최근 조제받은 환자 (중복 제외, 최대 3명) + total_usage = 0 # 총 사용량 for row in result: dosage = float(row.dosage) if row.dosage else 0 freq = float(row.frequency) if row.frequency else 0 days = int(row.days) if row.days else 0 patient = row.patient_name or '' + qty = dosage * freq * days + total_usage += qty # 중복 제외 환자 목록 (최근순, 최대 3명) if patient and patient not in seen_patients: @@ -8955,7 +8970,7 @@ def api_drug_usage_prescriptions(drug_code): 'dosage': dosage, 'frequency': freq, 'days': days, - 'total_qty': dosage * freq * days + 'total_qty': qty }) return jsonify({ @@ -8964,6 +8979,8 @@ def api_drug_usage_prescriptions(drug_code): 'total_count': len(items), 'unique_patients': len(seen_patients), 'recent_patients': recent_patients, + 'total_usage': total_usage, + 'unit': unit, 'items': items }) except Exception as e: diff --git a/backend/templates/admin_drug_usage.html b/backend/templates/admin_drug_usage.html index 9a35013..b149144 100644 --- a/backend/templates/admin_drug_usage.html +++ b/backend/templates/admin_drug_usage.html @@ -392,6 +392,16 @@ background: #fef3c7; 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 { max-height: 300px; overflow-y: auto; @@ -805,14 +815,13 @@ mainRow.classList.add('expanded'); expandedDrugCode = drugCode; - // 데이터 로드 + // 데이터 로드 (순차 호출 - DB 세션 충돌 방지) const startDate = document.getElementById('startDate').value.replace(/-/g, ''); const endDate = document.getElementById('endDate').value.replace(/-/g, ''); - // 입고 데이터 로드 - loadImports(drugCode, startDate, endDate); - // 조제 데이터 로드 - loadPrescriptions(drugCode, startDate, endDate); + // 입고 데이터 로드 후 조제 데이터 로드 (순차) + await loadImports(drugCode, startDate, endDate); + await loadPrescriptions(drugCode, startDate, endDate); } // ═══ 입고 데이터 로드 ═══ @@ -901,8 +910,13 @@ ).join('') + `외 ${uniqueCount - 3}명`; } + // 총 사용량 + 단위 + const totalUsage = data.total_usage || 0; + const unit = data.unit || '개'; + const usageBadge = `${formatNumber(totalUsage)}${unit}`; + container.innerHTML = ` -

💊 조제목록 (${data.total_count}건) ${patientBadges}

+

💊 조제목록 (${data.total_count}건) ${usageBadge} ${patientBadges}

diff --git a/backend/utils/drug_unit.py b/backend/utils/drug_unit.py new file mode 100644 index 0000000..90896cf --- /dev/null +++ b/backend/utils/drug_unit.py @@ -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 '포'