feat(WEB): 범용 스켈레톤 시스템 구축
- GenericPageSkeleton 범용 컴포넌트 추가 (커스텀 페이지용) - AuthenticatedLayout 스피너 → 스켈레톤으로 변경 - (protected)/loading.tsx GenericPageSkeleton 적용 - subscription, account-info 페이지 개별 스켈레톤 추가 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { ListPageSkeleton } from '@/components/ui/skeleton';
|
||||
import { GenericPageSkeleton } from '@/components/ui/skeleton';
|
||||
|
||||
/**
|
||||
* Protected Group Loading UI
|
||||
@@ -7,8 +7,12 @@ import { ListPageSkeleton } from '@/components/ui/skeleton';
|
||||
* - AuthenticatedLayout 내에서 표시됨 (사이드바, 헤더 유지)
|
||||
* - React Suspense 자동 적용
|
||||
* - 페이지 전환 시 즉각적인 피드백
|
||||
* - 스켈레톤 UI로 레이아웃 유지하며 로딩 표시
|
||||
* - 범용 스켈레톤으로 모든 페이지 타입에 대응
|
||||
*
|
||||
* 참고:
|
||||
* - 리스트 페이지: IntegratedListTemplateV2 내부에서 상세 스켈레톤 처리
|
||||
* - 커스텀 페이지: 이 범용 스켈레톤이 표시됨
|
||||
*/
|
||||
export default function ProtectedLoading() {
|
||||
return <ListPageSkeleton />;
|
||||
return <GenericPageSkeleton />;
|
||||
}
|
||||
|
||||
@@ -1,9 +1,14 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
import { User } from 'lucide-react';
|
||||
import { AccountInfoClient } from '@/components/settings/AccountInfoManagement';
|
||||
import { getAccountInfo } from '@/components/settings/AccountInfoManagement/actions';
|
||||
import type { AccountInfo, TermsAgreement, MarketingConsent } from '@/components/settings/AccountInfoManagement/types';
|
||||
import { PageLayout } from '@/components/organisms/PageLayout';
|
||||
import { PageHeader } from '@/components/organisms/PageHeader';
|
||||
import { Card, CardContent, CardHeader } from '@/components/ui/card';
|
||||
import { Skeleton } from '@/components/ui/skeleton';
|
||||
|
||||
const DEFAULT_ACCOUNT_INFO: AccountInfo = {
|
||||
id: '',
|
||||
@@ -44,9 +49,72 @@ export default function AccountInfoPage() {
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center min-h-[400px]">
|
||||
<div className="text-muted-foreground">로딩 중...</div>
|
||||
</div>
|
||||
<PageLayout>
|
||||
<PageHeader
|
||||
title="계정정보"
|
||||
description="계정 정보를 관리합니다"
|
||||
icon={User}
|
||||
/>
|
||||
<div className="space-y-6">
|
||||
{/* 계정 정보 카드 스켈레톤 */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<Skeleton className="h-5 w-24" />
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-6">
|
||||
<div className="space-y-2">
|
||||
<Skeleton className="h-4 w-20" />
|
||||
<Skeleton className="h-24 w-24 rounded-full" />
|
||||
</div>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div className="space-y-2">
|
||||
<Skeleton className="h-4 w-16" />
|
||||
<Skeleton className="h-10 w-full" />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Skeleton className="h-4 w-16" />
|
||||
<Skeleton className="h-10 w-20" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div className="space-y-2">
|
||||
<Skeleton className="h-4 w-12" />
|
||||
<Skeleton className="h-10 w-full" />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Skeleton className="h-4 w-12" />
|
||||
<Skeleton className="h-10 w-full" />
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
{/* 약관 동의 정보 카드 스켈레톤 */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<Skeleton className="h-5 w-32" />
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-6">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div className="space-y-2">
|
||||
<Skeleton className="h-4 w-40" />
|
||||
<Skeleton className="h-4 w-32" />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Skeleton className="h-4 w-40" />
|
||||
<Skeleton className="h-4 w-32" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-4">
|
||||
<Skeleton className="h-4 w-36" />
|
||||
<div className="space-y-3 pl-4">
|
||||
<Skeleton className="h-5 w-full" />
|
||||
<Skeleton className="h-5 w-full" />
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</PageLayout>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,13 @@
|
||||
'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 { PageLayout } from '@/components/organisms/PageLayout';
|
||||
import { PageHeader } from '@/components/organisms/PageHeader';
|
||||
import { Card, CardContent } from '@/components/ui/card';
|
||||
import { Skeleton } from '@/components/ui/skeleton';
|
||||
|
||||
export default function SubscriptionPage() {
|
||||
const [data, setData] = useState<Awaited<ReturnType<typeof getSubscriptionData>>['data']>(undefined);
|
||||
@@ -18,9 +23,47 @@ export default function SubscriptionPage() {
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center min-h-[400px]">
|
||||
<div className="text-muted-foreground">로딩 중...</div>
|
||||
</div>
|
||||
<PageLayout>
|
||||
<PageHeader
|
||||
title="구독관리"
|
||||
description="구독 정보를 관리합니다"
|
||||
icon={CreditCard}
|
||||
/>
|
||||
{/* 헤더 액션 버튼 스켈레톤 */}
|
||||
<div className="flex justify-end gap-2 mb-4">
|
||||
<Skeleton className="h-10 w-32" />
|
||||
<Skeleton className="h-10 w-24" />
|
||||
</div>
|
||||
<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-20 mb-2" />
|
||||
<Skeleton className="h-6 w-32 mb-6" />
|
||||
<div className="space-y-6">
|
||||
{[1, 2, 3].map((i) => (
|
||||
<div key={i} className="flex items-center gap-4">
|
||||
<Skeleton className="h-4 w-24 flex-shrink-0" />
|
||||
<Skeleton className="h-2 flex-1" />
|
||||
<Skeleton className="h-4 w-24" />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</PageLayout>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -598,6 +598,65 @@ function PageHeaderSkeleton({
|
||||
);
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// 14. 범용 페이지 스켈레톤 (커스텀 페이지용)
|
||||
// ============================================
|
||||
/**
|
||||
* 모든 페이지에 대응하는 심플한 범용 스켈레톤
|
||||
* - 리스트 페이지: IntegratedListTemplateV2 내부에서 처리
|
||||
* - 커스텀 페이지: 이 스켈레톤 사용 (settings, qms 등)
|
||||
*/
|
||||
function GenericPageSkeleton() {
|
||||
return (
|
||||
<div className="p-3 md:p-6 space-y-6 animate-pulse">
|
||||
{/* 페이지 헤더 영역 */}
|
||||
<div className="flex items-center gap-3">
|
||||
<Skeleton className="h-10 w-10 rounded-lg" />
|
||||
<div className="space-y-2">
|
||||
<Skeleton className="h-7 w-40" />
|
||||
<Skeleton className="h-4 w-56" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 액션 버튼 영역 */}
|
||||
<div className="flex justify-end gap-2">
|
||||
<Skeleton className="h-10 w-24 rounded-md" />
|
||||
<Skeleton className="h-10 w-24 rounded-md" />
|
||||
</div>
|
||||
|
||||
{/* 콘텐츠 카드 영역 */}
|
||||
<Card>
|
||||
<CardContent className="p-6">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{Array.from({ length: 6 }).map((_, i) => (
|
||||
<div key={i} className="space-y-2">
|
||||
<Skeleton className="h-4 w-20" />
|
||||
<Skeleton className="h-10 w-full rounded-md" />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* 추가 콘텐츠 카드 */}
|
||||
<Card>
|
||||
<CardContent className="p-6">
|
||||
<Skeleton className="h-4 w-32 mb-4" />
|
||||
<div className="space-y-4">
|
||||
{Array.from({ length: 3 }).map((_, i) => (
|
||||
<div key={i} className="flex items-center gap-4">
|
||||
<Skeleton className="h-4 w-24 flex-shrink-0" />
|
||||
<Skeleton className="h-2 flex-1 rounded-full" />
|
||||
<Skeleton className="h-4 w-20" />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Export
|
||||
// ============================================
|
||||
@@ -615,4 +674,5 @@ export {
|
||||
ListPageSkeleton,
|
||||
PageHeaderSkeleton,
|
||||
ContentSkeleton,
|
||||
GenericPageSkeleton,
|
||||
};
|
||||
|
||||
@@ -477,13 +477,43 @@ export default function AuthenticatedLayout({ children }: AuthenticatedLayoutPro
|
||||
// By removing this check, we allow the component to render immediately with default values
|
||||
// and update once hydration completes through the useEffect above.
|
||||
|
||||
// 🔧 새로고침 시 스피너 표시 (hydration 전)
|
||||
// 🔧 새로고침 시 스켈레톤 표시 (hydration 전)
|
||||
// GenericPageSkeleton import는 상단에 추가 필요
|
||||
if (!isMounted) {
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center bg-background">
|
||||
<div className="text-center">
|
||||
<div className="inline-block h-10 w-10 animate-spin rounded-full border-4 border-solid border-primary border-r-transparent mb-4"></div>
|
||||
<p className="text-muted-foreground text-sm">로딩 중...</p>
|
||||
<div className="min-h-screen flex flex-col w-full">
|
||||
{/* 헤더 스켈레톤 */}
|
||||
<div className="px-8 py-5 mx-3 mt-3 mb-0 rounded-2xl bg-muted/50 animate-pulse h-[82px]" />
|
||||
|
||||
{/* 사이드바 + 콘텐츠 스켈레톤 */}
|
||||
<div className="flex flex-1 gap-3 px-3 pb-3">
|
||||
{/* 사이드바 스켈레톤 */}
|
||||
<div className="hidden md:block w-64 mt-3">
|
||||
<div className="h-[calc(100vh-118px)] rounded-2xl bg-muted/50 animate-pulse" />
|
||||
</div>
|
||||
|
||||
{/* 콘텐츠 스켈레톤 */}
|
||||
<div className="flex-1 p-3 md:p-6 space-y-6 animate-pulse">
|
||||
{/* 페이지 헤더 */}
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="h-10 w-10 rounded-lg bg-muted" />
|
||||
<div className="space-y-2">
|
||||
<div className="h-7 w-40 rounded bg-muted" />
|
||||
<div className="h-4 w-56 rounded bg-muted" />
|
||||
</div>
|
||||
</div>
|
||||
{/* 콘텐츠 카드 */}
|
||||
<div className="rounded-lg border bg-card p-6">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{[1, 2, 3, 4, 5, 6].map((i) => (
|
||||
<div key={i} className="space-y-2">
|
||||
<div className="h-4 w-20 rounded bg-muted" />
|
||||
<div className="h-10 w-full rounded bg-muted" />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user