PharmQ SaaS 구독 서비스 관리 시스템 완전 구현
📋 기획 및 설계: - 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>
This commit is contained in:
@@ -40,6 +40,7 @@
|
||||
<tr>
|
||||
<th>약국 정보</th>
|
||||
<th>담당자</th>
|
||||
<th>구독 서비스</th>
|
||||
<th>연결된 머신</th>
|
||||
<th>네트워크 상태</th>
|
||||
<th>액션</th>
|
||||
@@ -68,6 +69,15 @@
|
||||
<i class="fas fa-phone"></i> {{ pharmacy_data.phone or '연락처 미등록' }}
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="subscription-services" data-pharmacy-id="{{ pharmacy_data.id }}">
|
||||
<div class="d-flex justify-content-center">
|
||||
<div class="spinner-border spinner-border-sm text-primary" role="status">
|
||||
<span class="visually-hidden">로딩...</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="d-flex align-items-center">
|
||||
<span class="badge bg-info me-2">{{ pharmacy_data.machine_count }}대</span>
|
||||
@@ -200,6 +210,9 @@ let pharmacyModal;
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
pharmacyModal = new bootstrap.Modal(document.getElementById('pharmacyModal'));
|
||||
|
||||
// 구독 상태 로드
|
||||
setTimeout(loadSubscriptionStatuses, 500); // 페이지 로드 후 약간의 지연
|
||||
});
|
||||
|
||||
function showAddModal() {
|
||||
@@ -316,6 +329,94 @@ document.getElementById('pharmacyForm').addEventListener('submit', function(e) {
|
||||
}
|
||||
});
|
||||
|
||||
// 구독 서비스 상태 로드
|
||||
function loadSubscriptionStatuses() {
|
||||
const subscriptionContainers = document.querySelectorAll('.subscription-services');
|
||||
|
||||
subscriptionContainers.forEach(container => {
|
||||
const pharmacyId = container.dataset.pharmacyId;
|
||||
|
||||
fetch(`/api/pharmacy/${pharmacyId}/subscriptions`)
|
||||
.then(response => response.json())
|
||||
.then(result => {
|
||||
if (result.success) {
|
||||
displaySubscriptionStatus(container, result.data);
|
||||
} else {
|
||||
showSubscriptionError(container);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error(`약국 ${pharmacyId} 구독 정보 로드 실패:`, error);
|
||||
showSubscriptionError(container);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function displaySubscriptionStatus(container, data) {
|
||||
const subscriptions = data.active_subscriptions;
|
||||
|
||||
if (subscriptions.length === 0) {
|
||||
container.innerHTML = `
|
||||
<div class="text-center">
|
||||
<span class="badge bg-light text-muted">
|
||||
<i class="fas fa-minus"></i> 구독 없음
|
||||
</span>
|
||||
</div>
|
||||
`;
|
||||
return;
|
||||
}
|
||||
|
||||
// 서비스 아이콘 맵핑
|
||||
const serviceIcons = {
|
||||
'CLOUD_PC': { icon: '💻', color: 'primary', name: '클라우드PC' },
|
||||
'AI_CCTV': { icon: '📷', color: 'info', name: 'AI CCTV' },
|
||||
'CRM': { icon: '📊', color: 'warning', name: 'CRM' }
|
||||
};
|
||||
|
||||
let html = '<div class="d-flex flex-wrap gap-1">';
|
||||
let totalFee = 0;
|
||||
|
||||
subscriptions.forEach(sub => {
|
||||
const service = serviceIcons[sub.code] || { icon: '📦', color: 'secondary', name: sub.name };
|
||||
totalFee += sub.monthly_fee;
|
||||
|
||||
html += `
|
||||
<span class="badge bg-${service.color}" title="${service.name} - ₩${sub.monthly_fee.toLocaleString()}/월" data-bs-toggle="tooltip">
|
||||
${service.icon}
|
||||
</span>
|
||||
`;
|
||||
});
|
||||
|
||||
html += '</div>';
|
||||
|
||||
// 총 월 구독료 표시
|
||||
html += `
|
||||
<div class="small text-muted text-center mt-1">
|
||||
<strong>₩${totalFee.toLocaleString()}/월</strong>
|
||||
</div>
|
||||
`;
|
||||
|
||||
container.innerHTML = html;
|
||||
|
||||
// 툴팁 초기화
|
||||
const tooltipTriggerList = container.querySelectorAll('[data-bs-toggle="tooltip"]');
|
||||
tooltipTriggerList.forEach(tooltipTriggerEl => {
|
||||
new bootstrap.Tooltip(tooltipTriggerEl);
|
||||
});
|
||||
}
|
||||
|
||||
function showSubscriptionError(container) {
|
||||
container.innerHTML = `
|
||||
<div class="text-center">
|
||||
<span class="badge bg-danger">
|
||||
<i class="fas fa-exclamation-triangle"></i> 오류
|
||||
</span>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
// 구독 상태 로드 함수들은 위의 DOMContentLoaded에서 호출됨
|
||||
|
||||
// 테이블 정렬 및 검색 기능 추가 (향후)
|
||||
</script>
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user