📋 기획 및 설계: - PharmQ SaaS 서비스 기획서 작성 - 구독 서비스 라인업 정의 (클라우드PC, AI CCTV, CRM) - DB 스키마 설계 및 API 아키텍처 설계 🗄️ 데이터베이스 구조: - service_products: 서비스 상품 마스터 테이블 - pharmacy_subscriptions: 약국별 구독 현황 테이블 - subscription_usage_logs: 서비스 이용 로그 테이블 - billing_history: 결제 이력 테이블 - 샘플 데이터 자동 생성 (21개 구독, 월 118만원 매출) 🔧 백엔드 API 구현: - 구독 현황 통계 API (/api/subscriptions/stats) - 약국별 구독 조회 API (/api/pharmacies/subscriptions) - 구독 상세 정보 API (/api/pharmacy/{id}/subscriptions) - 구독 생성/해지 API (/api/subscriptions) 🖥️ 프론트엔드 UI 구현: - 대시보드 구독 현황 카드 (월 매출, 구독 수, 구독률 등) - 약국 목록에 구독 상태 아이콘 및 월 구독료 표시 - 약국 상세 페이지 구독 서비스 섹션 추가 - 실시간 구독 생성/해지 기능 구현 ✨ 주요 특징: - 서비스별 색상 코딩 및 이모지 아이콘 시스템 - 실시간 업데이트 (구독 생성/해지 즉시 반영) - 반응형 디자인 (모바일/태블릿 최적화) - 툴팁 기반 상세 정보 표시 📊 현재 구독 현황: - 총 월 매출: ₩1,180,000 - 구독 약국: 10/14개 (71.4%) - AI CCTV: 6개 약국, CRM: 10개 약국, 클라우드PC: 5개 약국 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
347 lines
13 KiB
Python
347 lines
13 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
PharmQ SaaS 구독 서비스 데이터베이스 테이블 생성 스크립트
|
|
"""
|
|
|
|
import sqlite3
|
|
import os
|
|
from datetime import datetime
|
|
|
|
def create_subscription_tables():
|
|
"""구독 서비스 관련 테이블 생성"""
|
|
|
|
# FARMQ 데이터베이스 연결
|
|
db_path = '/srv/headscale-setup/farmq-admin/farmq.db'
|
|
conn = sqlite3.connect(db_path)
|
|
cursor = conn.cursor()
|
|
|
|
print("🚀 PharmQ SaaS 구독 서비스 테이블 생성 중...")
|
|
|
|
try:
|
|
# 1. service_products - 서비스 상품 마스터
|
|
print("📦 service_products 테이블 생성 중...")
|
|
cursor.execute('''
|
|
CREATE TABLE IF NOT EXISTS service_products (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
product_code VARCHAR(20) UNIQUE NOT NULL,
|
|
product_name VARCHAR(100) NOT NULL,
|
|
description TEXT,
|
|
monthly_price DECIMAL(10,2) NOT NULL,
|
|
setup_fee DECIMAL(10,2) DEFAULT 0,
|
|
is_active BOOLEAN DEFAULT TRUE,
|
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
)
|
|
''')
|
|
|
|
# 2. pharmacy_subscriptions - 약국별 구독 현황
|
|
print("🏥 pharmacy_subscriptions 테이블 생성 중...")
|
|
cursor.execute('''
|
|
CREATE TABLE IF NOT EXISTS pharmacy_subscriptions (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
pharmacy_id INTEGER NOT NULL,
|
|
product_id INTEGER NOT NULL,
|
|
subscription_status VARCHAR(20) NOT NULL DEFAULT 'ACTIVE',
|
|
start_date DATE NOT NULL,
|
|
end_date DATE,
|
|
next_billing_date DATE,
|
|
monthly_fee DECIMAL(10,2) NOT NULL,
|
|
notes TEXT,
|
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
FOREIGN KEY (pharmacy_id) REFERENCES pharmacies(id),
|
|
FOREIGN KEY (product_id) REFERENCES service_products(id),
|
|
UNIQUE(pharmacy_id, product_id)
|
|
)
|
|
''')
|
|
|
|
# 3. subscription_usage_logs - 서비스 이용 로그
|
|
print("📊 subscription_usage_logs 테이블 생성 중...")
|
|
cursor.execute('''
|
|
CREATE TABLE IF NOT EXISTS subscription_usage_logs (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
subscription_id INTEGER NOT NULL,
|
|
usage_type VARCHAR(50) NOT NULL,
|
|
usage_amount INTEGER DEFAULT 1,
|
|
usage_date DATE NOT NULL,
|
|
metadata TEXT,
|
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
FOREIGN KEY (subscription_id) REFERENCES pharmacy_subscriptions(id)
|
|
)
|
|
''')
|
|
|
|
# 4. billing_history - 결제 이력
|
|
print("💳 billing_history 테이블 생성 중...")
|
|
cursor.execute('''
|
|
CREATE TABLE IF NOT EXISTS billing_history (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
subscription_id INTEGER NOT NULL,
|
|
billing_period_start DATE NOT NULL,
|
|
billing_period_end DATE NOT NULL,
|
|
amount DECIMAL(10,2) NOT NULL,
|
|
billing_status VARCHAR(20) NOT NULL DEFAULT 'PENDING',
|
|
billing_date DATE,
|
|
payment_method VARCHAR(50),
|
|
invoice_number VARCHAR(100),
|
|
notes TEXT,
|
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
FOREIGN KEY (subscription_id) REFERENCES pharmacy_subscriptions(id)
|
|
)
|
|
''')
|
|
|
|
# 인덱스 생성
|
|
print("🔍 인덱스 생성 중...")
|
|
indexes = [
|
|
"CREATE INDEX IF NOT EXISTS idx_pharmacy_subscriptions_pharmacy_id ON pharmacy_subscriptions(pharmacy_id)",
|
|
"CREATE INDEX IF NOT EXISTS idx_pharmacy_subscriptions_product_id ON pharmacy_subscriptions(product_id)",
|
|
"CREATE INDEX IF NOT EXISTS idx_pharmacy_subscriptions_status ON pharmacy_subscriptions(subscription_status)",
|
|
"CREATE INDEX IF NOT EXISTS idx_usage_logs_subscription_id ON subscription_usage_logs(subscription_id)",
|
|
"CREATE INDEX IF NOT EXISTS idx_usage_logs_date ON subscription_usage_logs(usage_date)",
|
|
"CREATE INDEX IF NOT EXISTS idx_billing_subscription_id ON billing_history(subscription_id)",
|
|
"CREATE INDEX IF NOT EXISTS idx_billing_status ON billing_history(billing_status)"
|
|
]
|
|
|
|
for index_sql in indexes:
|
|
cursor.execute(index_sql)
|
|
|
|
conn.commit()
|
|
print("✅ 모든 테이블이 성공적으로 생성되었습니다!")
|
|
|
|
except Exception as e:
|
|
print(f"❌ 오류 발생: {e}")
|
|
conn.rollback()
|
|
raise
|
|
finally:
|
|
conn.close()
|
|
|
|
def insert_sample_service_products():
|
|
"""기본 서비스 상품 데이터 삽입"""
|
|
|
|
db_path = '/srv/headscale-setup/farmq-admin/farmq.db'
|
|
conn = sqlite3.connect(db_path)
|
|
cursor = conn.cursor()
|
|
|
|
print("📦 기본 서비스 상품 데이터 삽입 중...")
|
|
|
|
# 기본 서비스 상품 정의
|
|
products = [
|
|
{
|
|
'product_code': 'CLOUD_PC',
|
|
'product_name': '클라우드 PC',
|
|
'description': 'Proxmox 기반 가상 데스크톱 서비스. 언제 어디서나 안전한 클라우드 환경에서 업무 처리 가능.',
|
|
'monthly_price': 60000.00,
|
|
'setup_fee': 0.00
|
|
},
|
|
{
|
|
'product_code': 'AI_CCTV',
|
|
'product_name': 'AI CCTV',
|
|
'description': '인공지능 기반 보안 모니터링 시스템. 실시간 이상 상황 탐지 및 알림 서비스.',
|
|
'monthly_price': 80000.00,
|
|
'setup_fee': 50000.00
|
|
},
|
|
{
|
|
'product_code': 'CRM',
|
|
'product_name': 'CRM 시스템',
|
|
'description': '고객 관계 관리 및 매출 분석 도구. 고객 데이터 통합 관리 및 마케팅 자동화.',
|
|
'monthly_price': 40000.00,
|
|
'setup_fee': 0.00
|
|
}
|
|
]
|
|
|
|
try:
|
|
for product in products:
|
|
# 중복 확인
|
|
cursor.execute("SELECT id FROM service_products WHERE product_code = ?", (product['product_code'],))
|
|
if cursor.fetchone():
|
|
print(f"⚠️ {product['product_name']} 상품이 이미 존재합니다.")
|
|
continue
|
|
|
|
cursor.execute('''
|
|
INSERT INTO service_products
|
|
(product_code, product_name, description, monthly_price, setup_fee)
|
|
VALUES (?, ?, ?, ?, ?)
|
|
''', (
|
|
product['product_code'],
|
|
product['product_name'],
|
|
product['description'],
|
|
product['monthly_price'],
|
|
product['setup_fee']
|
|
))
|
|
|
|
print(f"✅ {product['product_name']} 상품 추가됨 (월 {product['monthly_price']:,.0f}원)")
|
|
|
|
conn.commit()
|
|
print("✅ 기본 서비스 상품 데이터 삽입 완료!")
|
|
|
|
except Exception as e:
|
|
print(f"❌ 서비스 상품 데이터 삽입 오류: {e}")
|
|
conn.rollback()
|
|
raise
|
|
finally:
|
|
conn.close()
|
|
|
|
def create_sample_subscriptions():
|
|
"""샘플 구독 데이터 생성 (기존 약국 데이터 기반)"""
|
|
|
|
db_path = '/srv/headscale-setup/farmq-admin/farmq.db'
|
|
conn = sqlite3.connect(db_path)
|
|
cursor = conn.cursor()
|
|
|
|
print("🏥 샘플 구독 데이터 생성 중...")
|
|
|
|
try:
|
|
# 기존 약국 ID 조회
|
|
cursor.execute("SELECT id, pharmacy_name FROM pharmacies LIMIT 10")
|
|
pharmacies = cursor.fetchall()
|
|
|
|
# 서비스 상품 ID 조회
|
|
cursor.execute("SELECT id, product_code, monthly_price FROM service_products")
|
|
products = cursor.fetchall()
|
|
|
|
if not pharmacies:
|
|
print("⚠️ 약국 데이터가 없습니다. 구독 데이터를 생성할 수 없습니다.")
|
|
return
|
|
|
|
if not products:
|
|
print("⚠️ 서비스 상품 데이터가 없습니다.")
|
|
return
|
|
|
|
import random
|
|
from datetime import date, timedelta
|
|
|
|
subscription_count = 0
|
|
|
|
for pharmacy_id, pharmacy_name in pharmacies:
|
|
# 각 약국마다 랜덤하게 1-3개의 서비스 구독
|
|
num_subscriptions = random.randint(1, 3)
|
|
selected_products = random.sample(products, num_subscriptions)
|
|
|
|
for product_id, product_code, monthly_price in selected_products:
|
|
# 중복 구독 확인
|
|
cursor.execute('''
|
|
SELECT id FROM pharmacy_subscriptions
|
|
WHERE pharmacy_id = ? AND product_id = ?
|
|
''', (pharmacy_id, product_id))
|
|
|
|
if cursor.fetchone():
|
|
continue # 이미 구독 중
|
|
|
|
# 구독 시작일 (최근 1년 내 랜덤)
|
|
start_date = date.today() - timedelta(days=random.randint(0, 365))
|
|
|
|
# 다음 결제일 (시작일로부터 월 단위)
|
|
next_billing = start_date + timedelta(days=30)
|
|
if next_billing < date.today():
|
|
# 과거 날짜면 현재 날짜 기준으로 조정
|
|
days_since_start = (date.today() - start_date).days
|
|
months_passed = days_since_start // 30
|
|
next_billing = start_date + timedelta(days=(months_passed + 1) * 30)
|
|
|
|
cursor.execute('''
|
|
INSERT INTO pharmacy_subscriptions
|
|
(pharmacy_id, product_id, subscription_status, start_date,
|
|
next_billing_date, monthly_fee, notes)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
''', (
|
|
pharmacy_id,
|
|
product_id,
|
|
'ACTIVE',
|
|
start_date.isoformat(),
|
|
next_billing.isoformat(),
|
|
monthly_price,
|
|
f'{pharmacy_name}의 {product_code} 서비스 구독'
|
|
))
|
|
|
|
subscription_count += 1
|
|
print(f"✅ {pharmacy_name} → {product_code} 구독 추가 (월 {monthly_price:,.0f}원)")
|
|
|
|
conn.commit()
|
|
print(f"✅ 총 {subscription_count}개의 샘플 구독 데이터가 생성되었습니다!")
|
|
|
|
except Exception as e:
|
|
print(f"❌ 샘플 구독 데이터 생성 오류: {e}")
|
|
conn.rollback()
|
|
raise
|
|
finally:
|
|
conn.close()
|
|
|
|
def show_subscription_summary():
|
|
"""구독 현황 요약 출력"""
|
|
|
|
db_path = '/srv/headscale-setup/farmq-admin/farmq.db'
|
|
conn = sqlite3.connect(db_path)
|
|
cursor = conn.cursor()
|
|
|
|
print("\n" + "="*60)
|
|
print("📊 PharmQ SaaS 구독 현황 요약")
|
|
print("="*60)
|
|
|
|
try:
|
|
# 전체 구독 통계
|
|
cursor.execute('''
|
|
SELECT
|
|
sp.product_name,
|
|
COUNT(*) as subscription_count,
|
|
SUM(ps.monthly_fee) as total_monthly_revenue
|
|
FROM pharmacy_subscriptions ps
|
|
JOIN service_products sp ON ps.product_id = sp.id
|
|
WHERE ps.subscription_status = 'ACTIVE'
|
|
GROUP BY sp.product_name
|
|
ORDER BY total_monthly_revenue DESC
|
|
''')
|
|
|
|
results = cursor.fetchall()
|
|
total_revenue = 0
|
|
|
|
for product_name, count, revenue in results:
|
|
print(f"🔹 {product_name}: {count}개 약국 구독 (월 {revenue:,.0f}원)")
|
|
total_revenue += revenue
|
|
|
|
print(f"\n💰 총 월 매출: {total_revenue:,.0f}원")
|
|
|
|
# 약국별 구독 수
|
|
cursor.execute('''
|
|
SELECT COUNT(DISTINCT pharmacy_id) as subscribed_pharmacies
|
|
FROM pharmacy_subscriptions
|
|
WHERE subscription_status = 'ACTIVE'
|
|
''')
|
|
|
|
subscribed_pharmacies = cursor.fetchone()[0]
|
|
|
|
cursor.execute("SELECT COUNT(*) FROM pharmacies")
|
|
total_pharmacies = cursor.fetchone()[0]
|
|
|
|
print(f"🏥 구독 약국: {subscribed_pharmacies}/{total_pharmacies}개 ({subscribed_pharmacies/total_pharmacies*100:.1f}%)")
|
|
|
|
except Exception as e:
|
|
print(f"❌ 요약 정보 조회 오류: {e}")
|
|
finally:
|
|
conn.close()
|
|
print("="*60)
|
|
|
|
if __name__ == "__main__":
|
|
print("🚀 PharmQ SaaS 구독 서비스 데이터베이스 초기화")
|
|
print("-" * 60)
|
|
|
|
try:
|
|
# 1. 테이블 생성
|
|
create_subscription_tables()
|
|
print()
|
|
|
|
# 2. 기본 서비스 상품 삽입
|
|
insert_sample_service_products()
|
|
print()
|
|
|
|
# 3. 샘플 구독 데이터 생성
|
|
create_sample_subscriptions()
|
|
print()
|
|
|
|
# 4. 구독 현황 요약
|
|
show_subscription_summary()
|
|
|
|
print("\n🎉 PharmQ SaaS 구독 서비스 데이터베이스 초기화 완료!")
|
|
|
|
except Exception as e:
|
|
print(f"\n💥 초기화 실패: {e}")
|
|
exit(1) |