feat(WEB): 리스트 페이지 헤더 영역 스켈레톤 추가
- ListPageSkeleton에 showDateRange, showCreateButton props 추가 - 페이지 타이틀, 날짜 범위 선택기, 프리셋 버튼, 등록 버튼 스켈레톤 구현 - Skeleton 컴포넌트 대신 직접 div 사용하여 색상 가시성 개선 - IntegratedListTemplateV2에서 새 props 전달 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -5,7 +5,7 @@ import { LucideIcon, Trash2, Plus, Loader2 } from "lucide-react";
|
||||
import { DateRangeSelector } from "@/components/molecules/DateRangeSelector";
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
import { Tabs, TabsContent } from "@/components/ui/tabs";
|
||||
import { TableSkeleton, MobileCardGridSkeleton } from "@/components/ui/skeleton";
|
||||
import { TableSkeleton, MobileCardGridSkeleton, StatCardGridSkeleton, ListPageSkeleton } from "@/components/ui/skeleton";
|
||||
import { Table, TableBody, TableCell, TableFooter, TableHead, TableHeader, TableRow } from "@/components/ui/table";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import { Button } from "@/components/ui/button";
|
||||
@@ -483,6 +483,27 @@ export function IntegratedListTemplateV2<T = any>({
|
||||
setShowDeleteDialog(false);
|
||||
};
|
||||
|
||||
// 로딩 시 전체 페이지 스켈레톤 표시
|
||||
// - isLoading이 true면 전체 스켈레톤 표시 (헤더, 달력, 버튼, 카드, 테이블 모두)
|
||||
// - 이렇게 하면 "따닥" 현상 없이 매끄러운 로딩 경험 제공
|
||||
if (isLoading) {
|
||||
return (
|
||||
<PageLayout>
|
||||
<ListPageSkeleton
|
||||
showHeader={true}
|
||||
showDateRange={dateRangeSelector?.enabled ?? false}
|
||||
showCreateButton={!!createButton}
|
||||
showFilters={!hideSearch}
|
||||
showStats={stats !== undefined && stats.length > 0}
|
||||
statsCount={stats?.length || 4}
|
||||
tableRows={pagination.itemsPerPage || 10}
|
||||
tableColumns={tableColumns.length || 6}
|
||||
mobileCards={6}
|
||||
/>
|
||||
</PageLayout>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<PageLayout>
|
||||
{/* 페이지 헤더 */}
|
||||
@@ -530,11 +551,15 @@ export function IntegratedListTemplateV2<T = any>({
|
||||
)}
|
||||
|
||||
{/* 통계 카드 - 태블릿/데스크톱 */}
|
||||
{stats && stats.length > 0 && (
|
||||
{isLoading && stats !== undefined ? (
|
||||
<div className="hidden md:block">
|
||||
<StatCardGridSkeleton count={stats.length || 4} />
|
||||
</div>
|
||||
) : stats && stats.length > 0 ? (
|
||||
<div className="hidden md:block">
|
||||
<StatCards stats={stats} />
|
||||
</div>
|
||||
)}
|
||||
) : null}
|
||||
|
||||
{/* 경고 배너 (통계 카드와 검색 영역 사이) */}
|
||||
{alertBanner}
|
||||
|
||||
@@ -380,6 +380,8 @@ function StatCardGridSkeleton({
|
||||
// ============================================
|
||||
interface ListPageSkeletonProps {
|
||||
showHeader?: boolean;
|
||||
showDateRange?: boolean; // 달력 + 프리셋 버튼 영역
|
||||
showCreateButton?: boolean; // 등록 버튼
|
||||
showFilters?: boolean;
|
||||
showStats?: boolean;
|
||||
statsCount?: number;
|
||||
@@ -390,6 +392,8 @@ interface ListPageSkeletonProps {
|
||||
|
||||
function ListPageSkeleton({
|
||||
showHeader = true,
|
||||
showDateRange = true,
|
||||
showCreateButton = true,
|
||||
showFilters = true,
|
||||
showStats = false,
|
||||
statsCount = 4,
|
||||
@@ -398,15 +402,45 @@ function ListPageSkeleton({
|
||||
mobileCards = 6,
|
||||
}: ListPageSkeletonProps) {
|
||||
return (
|
||||
<div className="space-y-6 animate-pulse">
|
||||
{/* 페이지 헤더 */}
|
||||
<div className="space-y-6">
|
||||
{/* 페이지 헤더 (타이틀 + 설명) */}
|
||||
{showHeader && (
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="space-y-2">
|
||||
<Skeleton className="h-8 w-40" />
|
||||
<Skeleton className="h-4 w-56" />
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="h-10 w-10 rounded-lg bg-gray-200 animate-pulse" />
|
||||
<div className="space-y-1.5">
|
||||
<div className="h-7 w-40 rounded bg-gray-200 animate-pulse" />
|
||||
<div className="h-4 w-56 rounded bg-gray-100 animate-pulse" />
|
||||
</div>
|
||||
<Skeleton className="h-10 w-24 rounded-md" />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 헤더 액션 영역: 날짜 범위 선택기 + 프리셋 버튼 + 등록 버튼 */}
|
||||
{(showDateRange || showCreateButton) && (
|
||||
<div className="flex items-center gap-2 flex-wrap w-full">
|
||||
{/* 날짜 범위 선택기 (왼쪽) */}
|
||||
{showDateRange && (
|
||||
<>
|
||||
{/* 날짜 입력 (시작일 ~ 종료일) */}
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="h-10 w-[140px] rounded-md border border-gray-200 bg-gray-100 animate-pulse" />
|
||||
<div className="h-4 w-4 bg-gray-300 rounded animate-pulse" />
|
||||
<div className="h-10 w-[140px] rounded-md border border-gray-200 bg-gray-100 animate-pulse" />
|
||||
</div>
|
||||
{/* 프리셋 버튼들 (당해년도, 전전월, 전월, 당월, 어제, 오늘) */}
|
||||
<div className="hidden md:flex items-center gap-1.5">
|
||||
<div className="h-8 w-16 rounded-md border border-gray-200 bg-gray-100 animate-pulse" />
|
||||
<div className="h-8 w-14 rounded-md border border-gray-200 bg-gray-100 animate-pulse" />
|
||||
<div className="h-8 w-12 rounded-md border border-gray-200 bg-gray-100 animate-pulse" />
|
||||
<div className="h-8 w-12 rounded-md border border-gray-200 bg-gray-100 animate-pulse" />
|
||||
<div className="h-8 w-10 rounded-md border border-gray-200 bg-gray-100 animate-pulse" />
|
||||
<div className="h-8 w-10 rounded-md border border-gray-200 bg-gray-100 animate-pulse" />
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{/* 등록 버튼 (오른쪽 끝) */}
|
||||
{showCreateButton && (
|
||||
<div className="h-10 w-28 rounded-md border border-gray-200 bg-gray-100 animate-pulse ml-auto" />
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user