diff --git a/backend/app.py b/backend/app.py index af8f4a1..a700980 100644 --- a/backend/app.py +++ b/backend/app.py @@ -3877,6 +3877,336 @@ def api_sales_detail(): }), 500 +# ===== 사용량 조회 페이지 및 API ===== + +@app.route('/admin/usage') +def admin_usage(): + """OTC 사용량 조회 · 주문 페이지""" + return render_template('admin_usage.html') + + +@app.route('/admin/rx-usage') +def admin_rx_usage(): + """전문의약품 사용량 조회 · 주문 페이지""" + return render_template('admin_rx_usage.html') + + +@app.route('/api/usage') +def api_usage(): + """ + 기간별 품목 사용량 조회 API + GET /api/usage?start_date=2026-01-01&end_date=2026-01-31&search=타이레놀&sort=qty_desc + """ + try: + start_date = request.args.get('start_date', '') + end_date = request.args.get('end_date', '') + search = request.args.get('search', '').strip() + sort = request.args.get('sort', 'qty_desc') # qty_desc, qty_asc, name_asc, amount_desc + + # 날짜 형식 변환 (YYYY-MM-DD -> YYYYMMDD) + start_date_fmt = start_date.replace('-', '') if start_date else datetime.now().strftime('%Y%m%d') + end_date_fmt = end_date.replace('-', '') if end_date else datetime.now().strftime('%Y%m%d') + + mssql_session = db_manager.get_session('PM_PRES') + + # 품목별 사용량 집계 쿼리 + usage_query = text(""" + SELECT + S.DrugCode as drug_code, + ISNULL(G.GoodsName, '알 수 없음') as product_name, + CASE + WHEN G.SplName IS NOT NULL AND G.SplName != '' THEN G.SplName + WHEN SET_CHK.is_set = 1 THEN '세트상품' + ELSE '' + END as supplier, + SUM(ISNULL(S.QUAN, 1)) as total_qty, + SUM(ISNULL(S.SL_TOTAL_PRICE, 0)) as total_amount, + COALESCE(NULLIF(G.BARCODE, ''), U.CD_CD_BARCODE, '') as barcode + FROM SALE_SUB S + LEFT JOIN PM_DRUG.dbo.CD_GOODS G ON S.DrugCode = G.DrugCode + OUTER APPLY ( + SELECT TOP 1 CD_CD_BARCODE + FROM PM_DRUG.dbo.CD_ITEM_UNIT_MEMBER + WHERE DRUGCODE = S.DrugCode AND CD_CD_BARCODE IS NOT NULL AND CD_CD_BARCODE != '' + ) U + OUTER APPLY ( + SELECT TOP 1 1 as is_set + FROM PM_DRUG.dbo.CD_item_set + WHERE SetCode = S.DrugCode AND DrugCode = 'SET0000' + ) SET_CHK + WHERE S.SL_DT_appl >= :start_date + AND S.SL_DT_appl <= :end_date + GROUP BY S.DrugCode, G.GoodsName, G.SplName, SET_CHK.is_set, G.BARCODE, U.CD_CD_BARCODE + ORDER BY SUM(ISNULL(S.QUAN, 1)) DESC + """) + + rows = mssql_session.execute(usage_query, { + 'start_date': start_date_fmt, + 'end_date': end_date_fmt + }).fetchall() + + items = [] + total_qty = 0 + total_amount = 0 + + for row in rows: + drug_code = row.drug_code or '' + product_name = row.product_name or '' + + # 검색 필터 + if search: + search_lower = search.lower() + if (search_lower not in product_name.lower() and + search_lower not in drug_code.lower()): + continue + + qty = int(row.total_qty or 0) + amount = float(row.total_amount or 0) + + items.append({ + 'drug_code': drug_code, + 'product_name': product_name, + 'supplier': row.supplier or '', + 'barcode': row.barcode or '', + 'total_qty': qty, + 'total_amount': int(amount), + 'thumbnail': None + }) + + total_qty += qty + total_amount += amount + + # 정렬 + if sort == 'qty_asc': + items.sort(key=lambda x: x['total_qty']) + elif sort == 'qty_desc': + items.sort(key=lambda x: x['total_qty'], reverse=True) + elif sort == 'name_asc': + items.sort(key=lambda x: x['product_name']) + elif sort == 'amount_desc': + items.sort(key=lambda x: x['total_amount'], reverse=True) + + # 제품 이미지 조회 + try: + images_db_path = Path(__file__).parent / 'db' / 'product_images.db' + if images_db_path.exists(): + img_conn = sqlite3.connect(str(images_db_path)) + img_cursor = img_conn.cursor() + + barcodes = [item['barcode'] for item in items if item['barcode']] + drug_codes = [item['drug_code'] for item in items] + + image_map = {} + if barcodes: + placeholders = ','.join(['?' for _ in barcodes]) + img_cursor.execute(f''' + SELECT barcode, thumbnail_base64 + FROM product_images + WHERE barcode IN ({placeholders}) AND thumbnail_base64 IS NOT NULL + ''', barcodes) + for r in img_cursor.fetchall(): + image_map[f'bc:{r[0]}'] = r[1] + + if drug_codes: + placeholders = ','.join(['?' for _ in drug_codes]) + img_cursor.execute(f''' + SELECT drug_code, thumbnail_base64 + FROM product_images + WHERE drug_code IN ({placeholders}) AND thumbnail_base64 IS NOT NULL + ''', drug_codes) + for r in img_cursor.fetchall(): + if f'dc:{r[0]}' not in image_map: + image_map[f'dc:{r[0]}'] = r[1] + + img_conn.close() + + for item in items: + thumb = image_map.get(f'bc:{item["barcode"]}') or image_map.get(f'dc:{item["drug_code"]}') + if thumb: + item['thumbnail'] = thumb + except Exception as img_err: + logging.warning(f"제품 이미지 조회 오류: {img_err}") + + # 기간 일수 계산 + try: + from datetime import datetime as dt + start_dt = dt.strptime(start_date_fmt, '%Y%m%d') + end_dt = dt.strptime(end_date_fmt, '%Y%m%d') + period_days = (end_dt - start_dt).days + 1 + except: + period_days = 1 + + return jsonify({ + 'success': True, + 'items': items[:500], # 최대 500건 + 'stats': { + 'period_days': period_days, + 'product_count': len(items), + 'total_qty': total_qty, + 'total_amount': int(total_amount) + } + }) + + except Exception as e: + logging.error(f"사용량 조회 오류: {e}") + return jsonify({ + 'success': False, + 'error': str(e) + }), 500 + + +@app.route('/api/rx-usage') +def api_rx_usage(): + """ + 전문의약품(처방전) 기간별 사용량 조회 API + GET /api/rx-usage?start_date=2026-01-01&end_date=2026-01-31&search=타이레놀&sort=qty_desc + """ + try: + start_date = request.args.get('start_date', '') + end_date = request.args.get('end_date', '') + search = request.args.get('search', '').strip() + sort = request.args.get('sort', 'qty_desc') + + # 날짜 형식 변환 (YYYY-MM-DD -> YYYYMMDD) + start_date_fmt = start_date.replace('-', '') if start_date else datetime.now().strftime('%Y%m%d') + end_date_fmt = end_date.replace('-', '') if end_date else datetime.now().strftime('%Y%m%d') + + mssql_session = db_manager.get_session('PM_PRES') + + # 전문의약품 품목별 사용량 집계 쿼리 (현재고: IM_total.IM_QT_sale_debit) + rx_query = text(""" + SELECT + P.DrugCode as drug_code, + ISNULL(G.GoodsName, '알 수 없음') as product_name, + ISNULL(G.SplName, '') as supplier, + SUM(ISNULL(P.QUAN, 1)) as total_qty, + SUM(ISNULL(P.QUAN, 1) * ISNULL(P.Days, 1)) as total_dose, + SUM(ISNULL(P.DRUPRICE, 0) * ISNULL(P.QUAN, 1) * ISNULL(P.Days, 1)) as total_amount, + COUNT(DISTINCT P.PreSerial) as prescription_count, + COALESCE(NULLIF(G.BARCODE, ''), '') as barcode, + ISNULL(IT.IM_QT_sale_debit, 0) as current_stock + FROM PS_sub_pharm P + LEFT JOIN PM_DRUG.dbo.CD_GOODS G ON P.DrugCode = G.DrugCode + LEFT JOIN PM_DRUG.dbo.IM_total IT ON P.DrugCode = IT.DrugCode + WHERE P.Indate >= :start_date + AND P.Indate <= :end_date + GROUP BY P.DrugCode, G.GoodsName, G.SplName, G.BARCODE, IT.IM_QT_sale_debit + ORDER BY SUM(ISNULL(P.QUAN, 1) * ISNULL(P.Days, 1)) DESC + """) + + rows = mssql_session.execute(rx_query, { + 'start_date': start_date_fmt, + 'end_date': end_date_fmt + }).fetchall() + + items = [] + total_qty = 0 + total_dose = 0 + total_amount = 0 + total_prescriptions = set() + + for row in rows: + drug_code = row.drug_code or '' + product_name = row.product_name or '' + + # 검색 필터 + if search: + search_lower = search.lower() + if (search_lower not in product_name.lower() and + search_lower not in drug_code.lower()): + continue + + qty = int(row.total_qty or 0) + dose = int(row.total_dose or 0) + amount = float(row.total_amount or 0) + rx_count = int(row.prescription_count or 0) + + items.append({ + 'drug_code': drug_code, + 'product_name': product_name, + 'supplier': row.supplier or '', + 'barcode': row.barcode or '', + 'total_qty': qty, + 'total_dose': dose, # 총 투약량 (수량 x 일수) + 'total_amount': int(amount), + 'prescription_count': rx_count, + 'current_stock': int(row.current_stock or 0), # 현재고 + 'thumbnail': None + }) + + total_qty += qty + total_dose += dose + total_amount += amount + + # 정렬 + if sort == 'qty_asc': + items.sort(key=lambda x: x['total_dose']) + elif sort == 'qty_desc': + items.sort(key=lambda x: x['total_dose'], reverse=True) + elif sort == 'name_asc': + items.sort(key=lambda x: x['product_name']) + elif sort == 'amount_desc': + items.sort(key=lambda x: x['total_amount'], reverse=True) + elif sort == 'rx_desc': + items.sort(key=lambda x: x['prescription_count'], reverse=True) + + # 제품 이미지 조회 + try: + images_db_path = Path(__file__).parent / 'db' / 'product_images.db' + if images_db_path.exists(): + img_conn = sqlite3.connect(str(images_db_path)) + img_cursor = img_conn.cursor() + + drug_codes = [item['drug_code'] for item in items] + + image_map = {} + if drug_codes: + placeholders = ','.join(['?' for _ in drug_codes]) + img_cursor.execute(f''' + SELECT drug_code, thumbnail_base64 + FROM product_images + WHERE drug_code IN ({placeholders}) AND thumbnail_base64 IS NOT NULL + ''', drug_codes) + for r in img_cursor.fetchall(): + image_map[r[0]] = r[1] + + img_conn.close() + + for item in items: + if item['drug_code'] in image_map: + item['thumbnail'] = image_map[item['drug_code']] + except Exception as img_err: + logging.warning(f"제품 이미지 조회 오류: {img_err}") + + # 기간 일수 계산 + try: + from datetime import datetime as dt + start_dt = dt.strptime(start_date_fmt, '%Y%m%d') + end_dt = dt.strptime(end_date_fmt, '%Y%m%d') + period_days = (end_dt - start_dt).days + 1 + except: + period_days = 1 + + return jsonify({ + 'success': True, + 'items': items[:500], + 'stats': { + 'period_days': period_days, + 'product_count': len(items), + 'total_qty': total_qty, + 'total_dose': total_dose, + 'total_amount': int(total_amount) + } + }) + + except Exception as e: + logging.error(f"전문의약품 사용량 조회 오류: {e}") + return jsonify({ + 'success': False, + 'error': str(e) + }), 500 + + # ===== Claude 상태 API ===== @app.route('/api/claude-status') diff --git a/backend/templates/admin_rx_usage.html b/backend/templates/admin_rx_usage.html new file mode 100644 index 0000000..9e533aa --- /dev/null +++ b/backend/templates/admin_rx_usage.html @@ -0,0 +1,1121 @@ + + + + + + 전문의약품 사용량 · 주문 - 청춘약국 + + + + + + +
+
+
+

💊 전문의약품 사용량 · 주문

+

처방전 조제 데이터 기반 발주

+
+ +
+
+ +
+ + + + +
+
+
📅
+
-
+
조회 기간
+
+
+
💊
+
-
+
처방 품목
+
+
+
🔢
+
-
+
총 처방수량
+
+
+
📊
+
-
+
총 투약량
+
+
+
💰
+
-
+
총 약가
+
+
+
🛒
+
0
+
주문 품목
+
+
+ + +
+
+ Rx + 품목별 사용량 + +
+
+ + + +
+
+ + +
+ + + + + + + + + + + + + + + + + +
약품현재고처방횟수투약량약가주문수량
+
+
+
데이터 로딩 중...
+
+
+
+
+ + + + + +
+
+

🛒 전문약 주문

+ +
+
+
+
🛒
+
장바구니가 비었습니다
+
+
+ +
+ + +
+ + + + diff --git a/backend/templates/admin_usage.html b/backend/templates/admin_usage.html new file mode 100644 index 0000000..d4e8d0e --- /dev/null +++ b/backend/templates/admin_usage.html @@ -0,0 +1,1080 @@ + + + + + + 사용량 조회 · 주문 - 청춘약국 + + + + + + +
+
+
+

📊 사용량 조회 · 주문

+

기간별 판매 사용량 분석 및 발주

+
+ +
+
+ +
+ + + + +
+
+
📅
+
-
+
조회 기간
+
+
+
📦
+
-
+
판매 품목
+
+
+
🔢
+
-
+
총 판매수량
+
+
+
💰
+
-
+
총 매출액
+
+
+
🛒
+
0
+
주문 품목
+
+
+ + +
+
+ 📋 품목별 사용량 + +
+
+ + + +
+
+ + +
+ + + + + + + + + + + + + + + + +
상품공급업체판매수량매출액주문수량
+
+
+
데이터 로딩 중...
+
+
+
+
+ + + + + +
+
+

🛒 주문 장바구니

+ +
+
+
+
🛒
+
장바구니가 비었습니다
+
+
+ +
+ + +
+ + + +