fix: 홈 로또 카드 가격/플랜명 수정, 관리자 구독자 통계/구독 현황 추가
- 홈 카드: 월 4,900원 → 900원~, 플랜명 골드/플래티넘/다이아로 수정 - 관리자 대시보드: 활성 구독자 수 카드 추가 - 관리자 회원 목록: 구독 현황(플랜명, 만료일) 컬럼 추가 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -7,6 +7,7 @@ interface Stats {
|
||||
totalOrders: number;
|
||||
totalRevenue: number;
|
||||
pendingContacts: number;
|
||||
activeSubscribers: number;
|
||||
monthlyChart: Array<{ month: string; revenue: number }>;
|
||||
}
|
||||
|
||||
@@ -53,7 +54,7 @@ export default function AdminDashboard() {
|
||||
) : (
|
||||
<>
|
||||
{/* 통계 카드 */}
|
||||
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4 mb-6">
|
||||
<div className="grid grid-cols-2 lg:grid-cols-5 gap-4 mb-6">
|
||||
<StatCard
|
||||
label="총 회원 수"
|
||||
value={`${stats?.totalMembers ?? 0}명`}
|
||||
@@ -87,6 +88,17 @@ export default function AdminDashboard() {
|
||||
</svg>
|
||||
}
|
||||
/>
|
||||
<StatCard
|
||||
label="활성 구독자"
|
||||
value={`${stats?.activeSubscribers ?? 0}명`}
|
||||
color="bg-amber-500/20 text-amber-400"
|
||||
icon={
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2}
|
||||
d="M11.049 2.927c.3-.921 1.603-.921 1.902 0l1.519 4.674a1 1 0 00.95.69h4.915c.969 0 1.371 1.24.588 1.81l-3.976 2.888a1 1 0 00-.363 1.118l1.518 4.674c.3.922-.755 1.688-1.538 1.118l-3.976-2.888a1 1 0 00-1.176 0l-3.976 2.888c-.783.57-1.838-.197-1.538-1.118l1.518-4.674a1 1 0 00-.363-1.118l-3.976-2.888c-.784-.57-.38-1.81.588-1.81h4.914a1 1 0 00.951-.69l1.519-4.674z" />
|
||||
</svg>
|
||||
}
|
||||
/>
|
||||
<StatCard
|
||||
label="미처리 문의"
|
||||
value={`${stats?.pendingContacts ?? 0}건`}
|
||||
|
||||
@@ -9,8 +9,15 @@ interface Member {
|
||||
created_at: string;
|
||||
orderCount: number;
|
||||
totalPaid: number;
|
||||
activeSub: { product_id: string; status: string; expires_at: string } | null;
|
||||
}
|
||||
|
||||
const PLAN_LABELS: Record<string, string> = {
|
||||
lotto_gold: '🥇 골드',
|
||||
lotto_platinum: '💎 플래티넘',
|
||||
lotto_diamond: '👑 다이아',
|
||||
};
|
||||
|
||||
export default function AdminMembersPage() {
|
||||
const [members, setMembers] = useState<Member[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
@@ -65,6 +72,7 @@ export default function AdminMembersPage() {
|
||||
<th className="text-left px-5 py-3 text-slate-400 font-medium">이메일</th>
|
||||
<th className="text-left px-5 py-3 text-slate-400 font-medium">이름</th>
|
||||
<th className="text-left px-5 py-3 text-slate-400 font-medium">가입일</th>
|
||||
<th className="text-left px-5 py-3 text-slate-400 font-medium">구독</th>
|
||||
<th className="text-right px-5 py-3 text-slate-400 font-medium">결제 건수</th>
|
||||
<th className="text-right px-5 py-3 text-slate-400 font-medium">총 결제액</th>
|
||||
</tr>
|
||||
@@ -72,7 +80,7 @@ export default function AdminMembersPage() {
|
||||
<tbody>
|
||||
{filtered.length === 0 ? (
|
||||
<tr>
|
||||
<td colSpan={5} className="text-center py-12 text-slate-500">
|
||||
<td colSpan={6} className="text-center py-12 text-slate-500">
|
||||
회원 데이터가 없습니다
|
||||
</td>
|
||||
</tr>
|
||||
@@ -84,6 +92,16 @@ export default function AdminMembersPage() {
|
||||
<td className="px-5 py-3 text-slate-400">
|
||||
{new Date(m.created_at).toLocaleDateString('ko-KR')}
|
||||
</td>
|
||||
<td className="px-5 py-3">
|
||||
{m.activeSub ? (
|
||||
<div>
|
||||
<span className="text-xs font-semibold text-amber-400">{PLAN_LABELS[m.activeSub.product_id] ?? m.activeSub.product_id}</span>
|
||||
<div className="text-xs text-slate-500">{new Date(m.activeSub.expires_at).toLocaleDateString('ko-KR')} 만료</div>
|
||||
</div>
|
||||
) : (
|
||||
<span className="text-xs text-slate-600">-</span>
|
||||
)}
|
||||
</td>
|
||||
<td className="px-5 py-3 text-right">
|
||||
<span className={`px-2 py-0.5 rounded-full text-xs ${m.orderCount > 0 ? 'bg-green-900/40 text-green-400' : 'bg-slate-700 text-slate-500'}`}>
|
||||
{m.orderCount}건
|
||||
|
||||
@@ -27,12 +27,14 @@ export async function GET() {
|
||||
// 각 회원의 주문 수 + 결제 금액 집계
|
||||
const enriched = await Promise.all(
|
||||
(profiles ?? []).map(async (p: { id: string; email: string; full_name: string; created_at: string }) => {
|
||||
const [ordersRes, paymentsRes] = await Promise.all([
|
||||
const [ordersRes, paymentsRes, subsRes] = await Promise.all([
|
||||
supabase.from('orders').select('id', { count: 'exact', head: true }).eq('user_id', p.id).eq('status', 'paid'),
|
||||
supabase.from('payments').select('amount').eq('user_id', p.id).eq('status', 'paid'),
|
||||
supabase.from('subscriptions').select('product_id, status, expires_at').eq('user_id', p.id).eq('status', 'active').order('created_at', { ascending: false }).limit(1),
|
||||
]);
|
||||
const totalPaid = (paymentsRes.data ?? []).reduce((s: number, x: { amount: number }) => s + x.amount, 0);
|
||||
return { ...p, orderCount: ordersRes.count ?? 0, totalPaid };
|
||||
const activeSub = subsRes.data?.[0] ?? null;
|
||||
return { ...p, orderCount: ordersRes.count ?? 0, totalPaid, activeSub };
|
||||
})
|
||||
);
|
||||
|
||||
|
||||
@@ -15,18 +15,20 @@ export async function GET() {
|
||||
const supabase = createAdminClient();
|
||||
|
||||
// 병렬 쿼리
|
||||
const [profilesRes, ordersRes, paymentsRes, contactsRes, monthlyRes] = await Promise.all([
|
||||
const [profilesRes, ordersRes, paymentsRes, contactsRes, monthlyRes, subsRes] = await Promise.all([
|
||||
supabase.from('profiles').select('id', { count: 'exact', head: true }),
|
||||
supabase.from('orders').select('id', { count: 'exact', head: true }).eq('status', 'paid'),
|
||||
supabase.from('payments').select('amount').eq('status', 'paid'),
|
||||
supabase.from('contact_requests').select('id', { count: 'exact', head: true }).eq('status', 'pending'),
|
||||
supabase.from('payments').select('amount, created_at').eq('status', 'paid').order('created_at', { ascending: true }),
|
||||
supabase.from('subscriptions').select('id', { count: 'exact', head: true }).eq('status', 'active'),
|
||||
]);
|
||||
|
||||
const totalMembers = profilesRes.count ?? 0;
|
||||
const totalOrders = ordersRes.count ?? 0;
|
||||
const totalRevenue = (paymentsRes.data ?? []).reduce((sum: number, p: { amount: number }) => sum + p.amount, 0);
|
||||
const pendingContacts = contactsRes.count ?? 0;
|
||||
const activeSubscribers = subsRes.count ?? 0;
|
||||
|
||||
// 최근 6개월 월별 수익 집계
|
||||
const monthly: Record<string, number> = {};
|
||||
@@ -47,5 +49,5 @@ export async function GET() {
|
||||
|
||||
const monthlyChart = Object.entries(monthly).map(([month, revenue]) => ({ month, revenue }));
|
||||
|
||||
return NextResponse.json({ totalMembers, totalOrders, totalRevenue, pendingContacts, monthlyChart });
|
||||
return NextResponse.json({ totalMembers, totalOrders, totalRevenue, pendingContacts, activeSubscribers, monthlyChart });
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ const techStack = [
|
||||
|
||||
const CHECKLIST_MAP: Record<string, string[]> = {
|
||||
'로또 번호 추천': [
|
||||
'구독 플랜 선택 (기본 / 프리미엄 / 연간)',
|
||||
'구독 플랜 선택 (골드 900원 / 플래티넘 2,900원 / 다이아 9,900원)',
|
||||
'번호 수신 방법 (이메일 / 텔레그램)',
|
||||
'당첨 보장 없음 — 통계 기반 확률 최적화',
|
||||
'구독 취소는 이메일로 언제든 가능',
|
||||
@@ -160,7 +160,7 @@ export default function Home() {
|
||||
<div className="px-6 py-5">
|
||||
<p className="text-slate-600 text-sm leading-relaxed mb-4">출현 빈도, 패턴 분석, 핫/콜드 번호 알고리즘으로 매주 최적의 번호 조합을 제공합니다.</p>
|
||||
<div className="space-y-2 mb-5">
|
||||
{['출현 빈도 통계 분석', '핫넘버 / 콜드넘버', '매주 5개 조합 제공'].map(f => (
|
||||
{['몬테카를로 시뮬레이션 분석', '핫넘버 / 콜드넘버 통계', '골드/플래티넘/다이아 플랜'].map(f => (
|
||||
<div key={f} className="flex items-center gap-2 text-sm text-slate-700">
|
||||
<div className="w-4 h-4 rounded-full bg-amber-100 border border-amber-200 flex items-center justify-center flex-shrink-0"><div className="w-1.5 h-1.5 rounded-full bg-amber-500" /></div>
|
||||
{f}
|
||||
@@ -169,7 +169,7 @@ export default function Home() {
|
||||
</div>
|
||||
<div className="flex items-center justify-between pt-4 border-t border-slate-100">
|
||||
<div>
|
||||
<span className="text-[#04102b] font-extrabold text-lg">월 4,900원~</span>
|
||||
<span className="text-[#04102b] font-extrabold text-lg">월 900원~</span>
|
||||
<span className="ml-2 text-xs bg-amber-50 border border-amber-200 text-amber-700 px-2 py-0.5 rounded-full font-medium">구독형</span>
|
||||
</div>
|
||||
<span className="text-[#1a56db] text-sm font-semibold flex items-center gap-1">자세히 보기 →</span>
|
||||
|
||||
Reference in New Issue
Block a user