매출 대시보드 완전 구현 완료
- /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:
@@ -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):
|
||||
|
||||
Reference in New Issue
Block a user