매출 대시보드 완전 구현 완료

- /revenue 경로로 매출 대시보드 페이지 추가
- 4개 매출 분석 API 엔드포인트 구현:
  * /api/analytics/revenue/monthly - 12개월 매출 트렌드
  * /api/analytics/revenue/by-service - 서비스별 매출 비중
  * /api/analytics/pharmacy-ranking - 약국별 구독료 순위
  * /api/analytics/subscription-trends - 구독 성장/해지 트렌드
- Chart.js 기반 종합 시각화 대시보드:
  * 월별 매출 라인 차트
  * 서비스별 매출 도넛 차트
  * 약국 순위 테이블
  * 구독 트렌드 바 차트
- 실시간 데이터 로딩 및 오류 처리 구현

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-09-11 19:59:29 +09:00
parent e93f96abe4
commit 6116f3fd15
2 changed files with 764 additions and 0 deletions

View File

@@ -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):