refactor: [subscription] SubscriptionClient 제거 + 사용량 페이지 정리
This commit is contained in:
@@ -1,72 +1,16 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
import { CreditCard } from 'lucide-react';
|
||||
import { SubscriptionManagement } from '@/components/settings/SubscriptionManagement';
|
||||
import { getSubscriptionData } from '@/components/settings/SubscriptionManagement/actions';
|
||||
import type { SubscriptionInfo } from '@/components/settings/SubscriptionManagement/types';
|
||||
import { PageLayout } from '@/components/organisms/PageLayout';
|
||||
import { PageHeader } from '@/components/organisms/PageHeader';
|
||||
import { Card, CardContent } from '@/components/ui/card';
|
||||
import { Skeleton } from '@/components/ui/skeleton';
|
||||
/**
|
||||
* /subscription → /usage 리다이렉트
|
||||
*/
|
||||
|
||||
function SubscriptionSkeleton() {
|
||||
return (
|
||||
<PageLayout>
|
||||
<PageHeader
|
||||
title="구독관리"
|
||||
description="구독 정보와 사용량을 관리합니다"
|
||||
icon={CreditCard}
|
||||
/>
|
||||
<div className="space-y-6">
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
{[1, 2, 3].map((i) => (
|
||||
<Card key={i}>
|
||||
<CardContent className="pt-6">
|
||||
<Skeleton className="h-4 w-24 mb-2" />
|
||||
<Skeleton className="h-8 w-32" />
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
<Card>
|
||||
<CardContent className="pt-6">
|
||||
<Skeleton className="h-4 w-28 mb-4" />
|
||||
<div className="space-y-5">
|
||||
{[1, 2].map((i) => (
|
||||
<div key={i} className="space-y-2">
|
||||
<div className="flex justify-between">
|
||||
<Skeleton className="h-4 w-16" />
|
||||
<Skeleton className="h-4 w-24" />
|
||||
</div>
|
||||
<Skeleton className="h-2 w-full" />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardContent className="pt-6">
|
||||
<Skeleton className="h-4 w-32 mb-4" />
|
||||
<Skeleton className="h-2 w-full mb-4" />
|
||||
<Skeleton className="h-24 w-full" />
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</PageLayout>
|
||||
);
|
||||
}
|
||||
|
||||
export default function SubscriptionPage() {
|
||||
const [data, setData] = useState<SubscriptionInfo | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
export default function SubscriptionRedirect() {
|
||||
const router = useRouter();
|
||||
useEffect(() => {
|
||||
getSubscriptionData()
|
||||
.then(result => setData(result.data))
|
||||
.finally(() => setIsLoading(false));
|
||||
}, []);
|
||||
|
||||
if (isLoading) return <SubscriptionSkeleton />;
|
||||
return <SubscriptionManagement initialData={data} />;
|
||||
router.replace('/usage');
|
||||
}, [router]);
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -1,16 +1,72 @@
|
||||
'use client';
|
||||
|
||||
/**
|
||||
* /usage → /subscription 리다이렉트
|
||||
*/
|
||||
import { useEffect, useState } from 'react';
|
||||
import { CreditCard } from 'lucide-react';
|
||||
import { SubscriptionManagement } from '@/components/settings/SubscriptionManagement';
|
||||
import { getSubscriptionData } from '@/components/settings/SubscriptionManagement/actions';
|
||||
import type { SubscriptionInfo } from '@/components/settings/SubscriptionManagement/types';
|
||||
import { PageLayout } from '@/components/organisms/PageLayout';
|
||||
import { PageHeader } from '@/components/organisms/PageHeader';
|
||||
import { Card, CardContent } from '@/components/ui/card';
|
||||
import { Skeleton } from '@/components/ui/skeleton';
|
||||
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
export default function UsageRedirect() {
|
||||
const router = useRouter();
|
||||
useEffect(() => {
|
||||
router.replace('/subscription');
|
||||
}, [router]);
|
||||
return null;
|
||||
function UsageSkeleton() {
|
||||
return (
|
||||
<PageLayout>
|
||||
<PageHeader
|
||||
title="이용현황"
|
||||
description="구독 정보와 사용량을 관리합니다"
|
||||
icon={CreditCard}
|
||||
/>
|
||||
<div className="space-y-6">
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
{[1, 2, 3].map((i) => (
|
||||
<Card key={i}>
|
||||
<CardContent className="pt-6">
|
||||
<Skeleton className="h-4 w-24 mb-2" />
|
||||
<Skeleton className="h-8 w-32" />
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
<Card>
|
||||
<CardContent className="pt-6">
|
||||
<Skeleton className="h-4 w-28 mb-4" />
|
||||
<div className="space-y-5">
|
||||
{[1, 2].map((i) => (
|
||||
<div key={i} className="space-y-2">
|
||||
<div className="flex justify-between">
|
||||
<Skeleton className="h-4 w-16" />
|
||||
<Skeleton className="h-4 w-24" />
|
||||
</div>
|
||||
<Skeleton className="h-2 w-full" />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardContent className="pt-6">
|
||||
<Skeleton className="h-4 w-32 mb-4" />
|
||||
<Skeleton className="h-2 w-full mb-4" />
|
||||
<Skeleton className="h-24 w-full" />
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</PageLayout>
|
||||
);
|
||||
}
|
||||
|
||||
export default function UsagePage() {
|
||||
const [data, setData] = useState<SubscriptionInfo | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
getSubscriptionData()
|
||||
.then(result => setData(result.data))
|
||||
.finally(() => setIsLoading(false));
|
||||
}, []);
|
||||
|
||||
if (isLoading) return <UsageSkeleton />;
|
||||
return <SubscriptionManagement initialData={data} />;
|
||||
}
|
||||
|
||||
@@ -1,178 +0,0 @@
|
||||
'use client';
|
||||
|
||||
/**
|
||||
* SubscriptionClient — 대체 구독관리 컴포넌트 (SubscriptionManagement.tsx 사용 권장)
|
||||
* 기존 호환성 유지를 위해 보존
|
||||
*/
|
||||
|
||||
import { useState, useCallback } from 'react';
|
||||
import { CreditCard, Download, AlertTriangle } from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Card, CardContent } from '@/components/ui/card';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { ConfirmDialog } from '@/components/ui/confirm-dialog';
|
||||
import { PageLayout } from '@/components/organisms/PageLayout';
|
||||
import { PageHeader } from '@/components/organisms/PageHeader';
|
||||
import { toast } from 'sonner';
|
||||
import { usePermission } from '@/hooks/usePermission';
|
||||
import { cancelSubscription, requestDataExport } from './actions';
|
||||
import type { SubscriptionInfo } from './types';
|
||||
import { SUBSCRIPTION_STATUS_LABELS } from './types';
|
||||
import { formatKrw, getProgressColor } from './utils';
|
||||
import { formatAmountWon as formatCurrency } from '@/lib/utils/amount';
|
||||
|
||||
interface SubscriptionClientProps {
|
||||
initialData: SubscriptionInfo;
|
||||
}
|
||||
|
||||
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')}`;
|
||||
};
|
||||
|
||||
function ColoredProgress({ value }: { value: number }) {
|
||||
const color = getProgressColor(value);
|
||||
const clamped = Math.min(value, 100);
|
||||
return (
|
||||
<div className="relative h-2 w-full overflow-hidden rounded-full bg-gray-100">
|
||||
<div className={`h-full rounded-full transition-all ${color}`} style={{ width: `${clamped}%` }} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function SubscriptionClient({ initialData }: SubscriptionClientProps) {
|
||||
const { canExport } = usePermission();
|
||||
const [subscription, setSubscription] = useState<SubscriptionInfo>(initialData);
|
||||
const [showCancelDialog, setShowCancelDialog] = useState(false);
|
||||
const [isExporting, setIsExporting] = useState(false);
|
||||
const [isCancelling, setIsCancelling] = useState(false);
|
||||
|
||||
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]);
|
||||
|
||||
const userPercentage = subscription.userLimit ? (subscription.userCount / subscription.userLimit) * 100 : 30;
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageLayout>
|
||||
<PageHeader
|
||||
title="구독관리"
|
||||
description="구독 정보와 사용량을 관리합니다"
|
||||
icon={CreditCard}
|
||||
actions={
|
||||
<div className="flex items-center gap-2">
|
||||
{canExport && (
|
||||
<Button variant="outline" onClick={handleExportData} disabled={isExporting}>
|
||||
<Download className="w-4 h-4 mr-2" />
|
||||
{isExporting ? '처리 중...' : '자료 내보내기'}
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
variant="outline"
|
||||
className="border-red-200 text-red-600 hover:bg-red-50 hover:border-red-300"
|
||||
onClick={() => setShowCancelDialog(true)}
|
||||
disabled={subscription.status === 'cancelled'}
|
||||
>
|
||||
서비스 해지
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
|
||||
<div className="space-y-6">
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<Card>
|
||||
<CardContent className="pt-6">
|
||||
<div className="text-sm text-muted-foreground mb-1">요금제</div>
|
||||
<div className="text-2xl font-bold">{subscription.planName}</div>
|
||||
<div className="text-sm text-muted-foreground mt-2">시작: {formatDate(subscription.startedAt)}</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardContent className="pt-6">
|
||||
<div className="text-sm text-muted-foreground mb-1">구독 상태</div>
|
||||
<Badge variant={subscription.status === 'active' ? 'default' : 'secondary'}>
|
||||
{SUBSCRIPTION_STATUS_LABELS[subscription.status] || subscription.status}
|
||||
</Badge>
|
||||
{subscription.remainingDays != null && subscription.remainingDays > 0 && (
|
||||
<div className="text-sm text-muted-foreground mt-2">남은 일: {subscription.remainingDays}일</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardContent className="pt-6">
|
||||
<div className="text-sm text-muted-foreground mb-1">구독 금액</div>
|
||||
<div className="text-2xl font-bold">{formatCurrency(subscription.monthlyFee)}/월</div>
|
||||
<div className="text-sm text-muted-foreground mt-2">종료: {formatDate(subscription.endedAt)}</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<Card>
|
||||
<CardContent className="pt-6">
|
||||
<div className="text-sm text-muted-foreground mb-4">리소스 사용량</div>
|
||||
<div className="space-y-5">
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm text-muted-foreground">사용자</span>
|
||||
<span className="text-sm font-medium">
|
||||
{subscription.userCount}명 / {subscription.userLimit ? `${subscription.userLimit}명` : '무제한'}
|
||||
</span>
|
||||
</div>
|
||||
<ColoredProgress value={userPercentage} />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm text-muted-foreground">저장 공간</span>
|
||||
<span className="text-sm font-medium">
|
||||
{subscription.storageUsedFormatted} / {subscription.storageLimitFormatted}
|
||||
</span>
|
||||
</div>
|
||||
<ColoredProgress value={subscription.storagePercentage} />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm text-muted-foreground">AI 토큰</span>
|
||||
<span className="text-sm font-medium">{formatKrw(subscription.aiTokens.costKrw)}</span>
|
||||
</div>
|
||||
<ColoredProgress value={subscription.aiTokens.percentage} />
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</PageLayout>
|
||||
|
||||
<ConfirmDialog
|
||||
open={showCancelDialog}
|
||||
onOpenChange={setShowCancelDialog}
|
||||
onConfirm={handleCancelService}
|
||||
variant="destructive"
|
||||
title={<span className="flex items-center gap-2"><AlertTriangle className="w-5 h-5 text-red-500" />서비스 해지</span>}
|
||||
description={<>모든 데이터가 삭제되며 복구할 수 없습니다.<br /><span className="font-medium text-red-600">정말 서비스를 해지하시겠습니까?</span></>}
|
||||
confirmText="확인"
|
||||
loading={isCancelling}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -99,7 +99,7 @@ export function SubscriptionManagement({ initialData }: SubscriptionManagementPr
|
||||
try {
|
||||
const result = await requestDataExport('all');
|
||||
if (result.success) {
|
||||
toast.success('내보내기 요청이 등록되었습니다. 완료되면 알림을 보내드립니다.');
|
||||
toast.success('자료 내보내기가 완료되었습니다.');
|
||||
} else {
|
||||
toast.error(result.error || '내보내기 요청에 실패했습니다.');
|
||||
}
|
||||
@@ -144,7 +144,7 @@ export function SubscriptionManagement({ initialData }: SubscriptionManagementPr
|
||||
<>
|
||||
<PageLayout>
|
||||
<PageHeader
|
||||
title="구독관리"
|
||||
title="이용현황"
|
||||
description="구독 정보와 사용량을 관리합니다"
|
||||
icon={CreditCard}
|
||||
/>
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
export { SubscriptionManagement } from './SubscriptionManagement';
|
||||
export { SubscriptionClient } from './SubscriptionClient';
|
||||
export * from './types';
|
||||
export * from './actions';
|
||||
export * from './utils';
|
||||
|
||||
Reference in New Issue
Block a user