'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: 서비스 관리 ===== */}
서비스 관리
{isExporting ? '처리 중...' : '자료 내보내기'}
setShowCancelDialog(true)}
disabled={subscription.status === 'cancelled'}
>
서비스 해지
{/* ===== 서비스 해지 확인 다이얼로그 ===== */}
서비스 해지
}
description={
<>
모든 데이터가 삭제되며 복구할 수 없습니다.
정말 서비스를 해지하시겠습니까?
>
}
confirmText="확인"
loading={isCancelling}
/>
>
);
}