'use client'; /** * 구독관리 (구독관리 통합) 페이지 * * 4섹션: 구독정보 카드 / 리소스 사용량 / AI 토큰 사용량 / 서비스 관리 */ import { useState, useCallback } from 'react'; import { CreditCard, Download, AlertTriangle, Cpu } from 'lucide-react'; import { toast } from 'sonner'; import { Button } from '@/components/ui/button'; import { Badge } from '@/components/ui/badge'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Progress } from '@/components/ui/progress'; import { ConfirmDialog } from '@/components/ui/confirm-dialog'; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from '@/components/ui/table'; import { PageLayout } from '@/components/organisms/PageLayout'; import { PageHeader } from '@/components/organisms/PageHeader'; import type { SubscriptionInfo } from './types'; import { SUBSCRIPTION_STATUS_LABELS } from './types'; import { requestDataExport, cancelSubscription } from './actions'; import { formatTokenCount, formatKrw, formatPeriod, getProgressColor } from './utils'; import { formatAmountWon as formatCurrency } from '@/lib/utils/amount'; // ===== 날짜 포맷 ===== const formatDate = (dateStr: string | null): string => { if (!dateStr) return '-'; const date = new Date(dateStr); if (isNaN(date.getTime())) return '-'; return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`; }; // ===== 기본값 ===== const defaultSubscription: SubscriptionInfo = { plan: 'free', planName: '무료', monthlyFee: 0, status: 'pending', startedAt: null, endedAt: null, remainingDays: null, userCount: 0, userLimit: null, storageUsed: 0, storageLimit: 107_374_182_400, storageUsedFormatted: '0 B', storageLimitFormatted: '100 GB', storagePercentage: 0, aiTokens: { period: '', totalTokens: 0, limit: 1_000_000, percentage: 0, costKrw: 0, warningThreshold: 80, isOverLimit: false, byModel: [], }, }; // ===== 색상이 적용된 Progress Bar ===== function ColoredProgress({ value, className = '' }: { value: number; className?: string }) { const color = getProgressColor(value); const clampedValue = Math.min(value, 100); return (
); } interface SubscriptionManagementProps { initialData: SubscriptionInfo | null; } export function SubscriptionManagement({ initialData }: SubscriptionManagementProps) { const [subscription, setSubscription] = useState(initialData || defaultSubscription); const [showCancelDialog, setShowCancelDialog] = useState(false); const [isExporting, setIsExporting] = useState(false); const [isCancelling, setIsCancelling] = useState(false); const { aiTokens } = subscription; // ===== 자료 내보내기 ===== const handleExportData = useCallback(async () => { setIsExporting(true); try { const result = await requestDataExport('all'); if (result.success) { toast.success('자료 내보내기가 완료되었습니다.'); } else { toast.error(result.error || '내보내기 요청에 실패했습니다.'); } } catch { toast.error('서버 오류가 발생했습니다.'); } finally { setIsExporting(false); } }, []); // ===== 서비스 해지 ===== const handleCancelService = useCallback(async () => { if (!subscription.id) { toast.error('구독 정보를 찾을 수 없습니다.'); setShowCancelDialog(false); return; } setIsCancelling(true); try { const result = await cancelSubscription(subscription.id, '사용자 요청'); if (result.success) { toast.success('서비스가 해지되었습니다.'); setSubscription(prev => ({ ...prev, status: 'cancelled' })); } else { toast.error(result.error || '서비스 해지에 실패했습니다.'); } } catch { toast.error('서버 오류가 발생했습니다.'); } finally { setIsCancelling(false); setShowCancelDialog(false); } }, [subscription.id]); // ===== Progress 계산 ===== const userPercentage = subscription.userLimit ? (subscription.userCount / subscription.userLimit) * 100 : 30; return ( <>
{/* ===== 섹션 1: 구독 정보 카드 ===== */} {subscription.planName ? (
{/* 요금제 */}
요금제
{subscription.planName}
시작: {formatDate(subscription.startedAt)}
{/* 구독 상태 */}
구독 상태
{SUBSCRIPTION_STATUS_LABELS[subscription.status] || subscription.status}
{subscription.remainingDays != null && subscription.remainingDays > 0 && (
남은 일: {subscription.remainingDays}일
)}
{/* 구독 금액 */}
구독 금액
{formatCurrency(subscription.monthlyFee)}/월
종료: {formatDate(subscription.endedAt)}
) : ( 구독 정보가 없습니다. 관리자에게 문의하세요. )} {/* ===== 섹션 2: 리소스 사용량 ===== */} 리소스 사용량
{/* 사용자 */}
사용자 {subscription.userCount}명 / {subscription.userLimit ? `${subscription.userLimit}명` : '무제한'}
{/* 저장 공간 */}
저장 공간 {subscription.storageUsedFormatted} / {subscription.storageLimitFormatted}
{/* ===== 섹션 3: AI 토큰 사용량 ===== */}
AI 토큰 사용량 {aiTokens.period && ( — {formatPeriod(aiTokens.period)} )}
{aiTokens.isOverLimit && ( 한도 초과 — 초과분 실비 과금 )} {!aiTokens.isOverLimit && aiTokens.percentage >= aiTokens.warningThreshold && ( 기본 제공량의 {aiTokens.percentage.toFixed(0)}% 사용 중 )}
{/* 토큰 사용량 Progress */}
{formatTokenCount(aiTokens.totalTokens)} / {formatTokenCount(aiTokens.limit)} {aiTokens.percentage.toFixed(1)}%
{/* 총 비용 */}
총 비용: {formatKrw(aiTokens.costKrw)}
{/* 모델별 사용량 테이블 */} {aiTokens.byModel.length > 0 && (
모델별 사용량
모델 호출수 토큰 비용 {aiTokens.byModel.map((m) => ( {m.model} {m.requests.toLocaleString()} {formatTokenCount(m.total_tokens)} {formatKrw(m.cost_krw)} ))}
)} {/* 안내 문구 */}

※ 기본 제공: 월 {formatTokenCount(aiTokens.limit)} 토큰. 초과 시 실비 과금

※ 매월 1일 리셋, 잔여 토큰 이월 불가

{/* ===== 섹션 4: 서비스 관리 ===== */} 서비스 관리
{/* ===== 서비스 해지 확인 다이얼로그 ===== */} 서비스 해지 } description={ <> 모든 데이터가 삭제되며 복구할 수 없습니다.
정말 서비스를 해지하시겠습니까? } confirmText="확인" loading={isCancelling} /> ); }