diff --git a/farmq-admin/app.py b/farmq-admin/app.py index b580dc0..56a94ff 100644 --- a/farmq-admin/app.py +++ b/farmq-admin/app.py @@ -1147,6 +1147,227 @@ def create_app(config_name=None): """구독 서비스 관리 페이지""" return render_template('subscriptions/list.html') + # =================== 매출 대시보드 =================== + + @app.route('/revenue') + def revenue_dashboard(): + """매출 대시보드 페이지""" + return render_template('revenue/dashboard.html') + + @app.route('/api/analytics/revenue/monthly', methods=['GET']) + def api_monthly_revenue(): + """월별 매출 통계""" + try: + import sqlite3 + from datetime import datetime, timedelta + import calendar + + db_path = '/srv/headscale-setup/farmq-admin/farmq.db' + conn = sqlite3.connect(db_path) + cursor = conn.cursor() + + # 최근 12개월 매출 데이터 생성 + current_date = datetime.now() + monthly_data = [] + + for i in range(12): + # i개월 전 날짜 계산 + target_date = current_date - timedelta(days=30*i) + year = target_date.year + month = target_date.month + month_name = calendar.month_name[month] + + # 해당 월의 구독 수 및 매출 계산 + cursor.execute(''' + SELECT + COUNT(ps.id) as subscription_count, + SUM(ps.monthly_fee) as total_revenue + FROM pharmacy_subscriptions ps + WHERE ps.subscription_status = 'ACTIVE' + AND ps.start_date <= ? + ''', (f'{year}-{month:02d}-28',)) + + result = cursor.fetchone() + subscription_count = result[0] or 0 + total_revenue = result[1] or 0 + + monthly_data.insert(0, { + 'month': f'{year}-{month:02d}', + 'month_name': f'{month_name[:3]} {year}', + 'subscription_count': subscription_count, + 'revenue': total_revenue + }) + + conn.close() + + return jsonify({ + 'success': True, + 'data': monthly_data + }) + + except Exception as e: + print(f"❌ 월별 매출 통계 조회 오류: {e}") + return jsonify({'success': False, 'error': str(e)}), 500 + + @app.route('/api/analytics/revenue/by-service', methods=['GET']) + def api_revenue_by_service(): + """서비스별 매출 통계""" + try: + import sqlite3 + + db_path = '/srv/headscale-setup/farmq-admin/farmq.db' + conn = sqlite3.connect(db_path) + cursor = conn.cursor() + + # 서비스별 매출 통계 + cursor.execute(''' + SELECT + sp.product_code, + sp.product_name, + COUNT(ps.id) as subscription_count, + SUM(ps.monthly_fee) as total_revenue + FROM service_products sp + LEFT JOIN pharmacy_subscriptions ps ON sp.id = ps.product_id + AND ps.subscription_status = 'ACTIVE' + GROUP BY sp.id, sp.product_code, sp.product_name + ORDER BY total_revenue DESC NULLS LAST + ''') + + services = [] + total_revenue = 0 + + for row in cursor.fetchall(): + product_code, product_name, count, revenue = row + revenue = revenue or 0 + total_revenue += revenue + + services.append({ + 'code': product_code, + 'name': product_name, + 'subscription_count': count, + 'revenue': revenue + }) + + # 비율 계산 + for service in services: + service['percentage'] = round((service['revenue'] / total_revenue * 100), 1) if total_revenue > 0 else 0 + + conn.close() + + return jsonify({ + 'success': True, + 'data': { + 'services': services, + 'total_revenue': total_revenue + } + }) + + except Exception as e: + print(f"❌ 서비스별 매출 통계 조회 오류: {e}") + return jsonify({'success': False, 'error': str(e)}), 500 + + @app.route('/api/analytics/pharmacy-ranking', methods=['GET']) + def api_pharmacy_ranking(): + """약국별 구독료 순위""" + try: + import sqlite3 + + db_path = '/srv/headscale-setup/farmq-admin/farmq.db' + conn = sqlite3.connect(db_path) + cursor = conn.cursor() + + # 약국별 구독료 순위 (상위 10개) + cursor.execute(''' + SELECT + p.pharmacy_name, + p.manager_name, + COUNT(ps.id) as service_count, + SUM(ps.monthly_fee) as total_monthly_fee + FROM pharmacies p + JOIN pharmacy_subscriptions ps ON p.id = ps.pharmacy_id + WHERE ps.subscription_status = 'ACTIVE' + GROUP BY p.id, p.pharmacy_name, p.manager_name + ORDER BY total_monthly_fee DESC + LIMIT 10 + ''') + + rankings = [] + for i, row in enumerate(cursor.fetchall(), 1): + pharmacy_name, manager_name, service_count, total_fee = row + rankings.append({ + 'rank': i, + 'pharmacy_name': pharmacy_name, + 'manager_name': manager_name, + 'service_count': service_count, + 'monthly_fee': total_fee + }) + + conn.close() + + return jsonify({ + 'success': True, + 'data': rankings + }) + + except Exception as e: + print(f"❌ 약국별 순위 조회 오류: {e}") + return jsonify({'success': False, 'error': str(e)}), 500 + + @app.route('/api/analytics/subscription-trends', methods=['GET']) + def api_subscription_trends(): + """구독 트렌드 통계 (신규/해지)""" + try: + import sqlite3 + from datetime import datetime, timedelta + + db_path = '/srv/headscale-setup/farmq-admin/farmq.db' + conn = sqlite3.connect(db_path) + cursor = conn.cursor() + + # 최근 6개월 구독 트렌드 + trends = [] + current_date = datetime.now() + + for i in range(6): + target_date = current_date - timedelta(days=30*i) + year = target_date.year + month = target_date.month + + # 해당 월 신규 구독 수 + cursor.execute(''' + SELECT COUNT(*) + FROM pharmacy_subscriptions + WHERE strftime('%Y-%m', start_date) = ? + ''', (f'{year}-{month:02d}',)) + new_subscriptions = cursor.fetchone()[0] + + # 해당 월 해지 구독 수 + cursor.execute(''' + SELECT COUNT(*) + FROM pharmacy_subscriptions + WHERE subscription_status = 'CANCELLED' + AND strftime('%Y-%m', updated_at) = ? + ''', (f'{year}-{month:02d}',)) + cancelled_subscriptions = cursor.fetchone()[0] + + trends.insert(0, { + 'month': f'{year}-{month:02d}', + 'new_subscriptions': new_subscriptions, + 'cancelled_subscriptions': cancelled_subscriptions, + 'net_growth': new_subscriptions - cancelled_subscriptions + }) + + conn.close() + + return jsonify({ + 'success': True, + 'data': trends + }) + + except Exception as e: + print(f"❌ 구독 트렌드 조회 오류: {e}") + return jsonify({'success': False, 'error': str(e)}), 500 + # 에러 핸들러 @app.errorhandler(404) def not_found_error(error): diff --git a/farmq-admin/templates/revenue/dashboard.html b/farmq-admin/templates/revenue/dashboard.html new file mode 100644 index 0000000..ff9105d --- /dev/null +++ b/farmq-admin/templates/revenue/dashboard.html @@ -0,0 +1,543 @@ +{% extends "base.html" %} + +{% block title %}매출 대시보드 - PharmQ Super Admin{% endblock %} + +{% block breadcrumb %} + +{% endblock %} + +{% block content %} + +
PharmQ SaaS 매출 현황 및 트렌드 분석
+매출 요약을 불러오는 중입니다...
+약국별 순위를 불러오는 중입니다...
+