refactor: UniversalListPage externalIsLoading 지원 및 스켈레톤 개선

- UniversalListPage에 externalIsLoading prop 추가
- CardTransactionDetailClient DevFill 자동입력 기능 추가
- 여러 컴포넌트 로딩 상태 처리 개선
- skeleton 컴포넌트 확장

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
유병철
2026-01-22 20:54:16 +09:00
parent 207520e1d6
commit 19237be4aa
71 changed files with 244 additions and 155 deletions

View File

@@ -7,7 +7,7 @@
import { useParams, useRouter } from 'next/navigation';
import { useEffect, useState } from 'react';
import { ContentLoadingSpinner } from '@/components/ui/loading-spinner';
import { DetailPageSkeleton } from '@/components/ui/skeleton';
import { BoardDetail } from '@/components/board/BoardDetail';
import { getPost } from '@/components/board/actions';
import type { Post, Comment } from '@/components/board/types';
@@ -60,7 +60,7 @@ export default function BoardDetailPage() {
}, [boardCode, postId, router]);
if (isLoading) {
return <ContentLoadingSpinner text="게시글을 불러오는 중..." />;
return <DetailPageSkeleton sections={1} fieldsPerSection={4} />;
}
if (!post) {

View File

@@ -9,7 +9,7 @@
import { useEffect } from 'react';
import { useParams, useRouter } from 'next/navigation';
import { ContentLoadingSpinner } from '@/components/ui/loading-spinner';
import { Skeleton } from '@/components/ui/skeleton';
export default function BoardEditRedirectPage() {
const router = useRouter();
@@ -20,5 +20,9 @@ export default function BoardEditRedirectPage() {
router.replace(`/ko/board/board-management/${id}?mode=edit`);
}, [router, id]);
return <ContentLoadingSpinner text="페이지 이동 중..." />;
return (
<div className="flex items-center justify-center h-64">
<Skeleton className="h-8 w-48" />
</div>
);
}

View File

@@ -7,7 +7,7 @@
import { useState, useEffect } from 'react';
import { useRouter, useParams } from 'next/navigation';
import { ArrowLeft, Save, MessageSquare } from 'lucide-react';
import { ContentLoadingSpinner } from '@/components/ui/loading-spinner';
import { DetailPageSkeleton } from '@/components/ui/skeleton';
import { PageLayout } from '@/components/organisms/PageLayout';
import { PageHeader } from '@/components/organisms/PageHeader';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
@@ -147,7 +147,7 @@ export default function DynamicBoardEditPage() {
if (isLoading) {
return (
<PageLayout>
<ContentLoadingSpinner text="게시글을 불러오는 중..." />
<DetailPageSkeleton />
</PageLayout>
);
}

View File

@@ -4,7 +4,7 @@ import { useEffect, useState } from 'react';
import WorkerStatusListClient from '@/components/business/construction/worker-status/WorkerStatusListClient';
import { getWorkerStatusList, getWorkerStatusStats } from '@/components/business/construction/worker-status/actions';
import type { WorkerStatus, WorkerStatusStats } from '@/components/business/construction/worker-status/types';
import { ContentLoadingSpinner } from '@/components/ui/loading-spinner';
import { ListPageSkeleton } from '@/components/ui/skeleton';
export default function WorkerStatusPage() {
const [data, setData] = useState<WorkerStatus[]>([]);
@@ -25,7 +25,7 @@ export default function WorkerStatusPage() {
}, []);
if (isLoading) {
return <ContentLoadingSpinner />;
return <ListPageSkeleton showHeader={false} showStats={true} statsCount={4} />;
}
return <WorkerStatusListClient initialData={data} initialStats={stats} />;

View File

@@ -10,7 +10,7 @@
import { Suspense } from 'react';
import { AttendanceManagement } from '@/components/hr/AttendanceManagement';
import { ContentLoadingSpinner } from '@/components/ui/loading-spinner';
import { ListPageSkeleton } from '@/components/ui/skeleton';
import type { Metadata } from 'next';
/**
@@ -23,7 +23,7 @@ export const metadata: Metadata = {
export default function AttendanceManagementPage() {
return (
<Suspense fallback={<ContentLoadingSpinner text="근태 정보를 불러오는 중..." />}>
<Suspense fallback={<ListPageSkeleton showHeader={false} showStats={true} statsCount={4} />}>
<AttendanceManagement />
</Suspense>
);

View File

@@ -7,7 +7,7 @@
import { Suspense } from 'react';
import { DepartmentManagement } from '@/components/hr/DepartmentManagement';
import { ContentLoadingSpinner } from '@/components/ui/loading-spinner';
import { ListPageSkeleton } from '@/components/ui/skeleton';
import type { Metadata } from 'next';
/**
@@ -20,7 +20,7 @@ export const metadata: Metadata = {
export default function DepartmentManagementPage() {
return (
<Suspense fallback={<ContentLoadingSpinner text="부서 정보를 불러오는 중..." />}>
<Suspense fallback={<ListPageSkeleton showHeader={false} />}>
<DepartmentManagement />
</Suspense>
);

View File

@@ -25,7 +25,7 @@ import {
SelectTrigger,
SelectValue,
} from '@/components/ui/select';
import { ContentLoadingSpinner } from '@/components/ui/loading-spinner';
import { FormSectionSkeleton } from '@/components/ui/skeleton';
import { format } from 'date-fns';
import { ko } from 'date-fns/locale';
import { toast } from 'sonner';
@@ -263,7 +263,7 @@ function DocumentNewContent() {
export default function DocumentNewPage() {
return (
<Suspense fallback={<ContentLoadingSpinner text="문서 양식을 불러오는 중..." />}>
<Suspense fallback={<FormSectionSkeleton rows={6} />}>
<DocumentNewContent />
</Suspense>
);

View File

@@ -6,7 +6,7 @@ import { EmployeeForm } from '@/components/hr/EmployeeManagement/EmployeeForm';
import { getEmployeeById, deleteEmployee, updateEmployee } from '@/components/hr/EmployeeManagement/actions';
import { ServerErrorPage } from '@/components/common/ServerErrorPage';
import { DeleteConfirmDialog } from '@/components/ui/confirm-dialog';
import { ContentLoadingSpinner } from '@/components/ui/loading-spinner';
import { DetailPageSkeleton } from '@/components/ui/skeleton';
import type { Employee, EmployeeFormData } from '@/components/hr/EmployeeManagement/types';
export default function EmployeeDetailPage() {
@@ -89,7 +89,7 @@ export default function EmployeeDetailPage() {
};
if (isLoading) {
return <ContentLoadingSpinner text="사원 정보를 불러오는 중..." />;
return <DetailPageSkeleton sections={2} fieldsPerSection={8} />;
}
if (!employee) {

View File

@@ -10,7 +10,7 @@
import { Suspense } from 'react';
import { EmployeeManagement } from '@/components/hr/EmployeeManagement';
import { ContentLoadingSpinner } from '@/components/ui/loading-spinner';
import { ListPageSkeleton } from '@/components/ui/skeleton';
import type { Metadata } from 'next';
/**
@@ -23,7 +23,7 @@ export const metadata: Metadata = {
export default function EmployeeManagementPage() {
return (
<Suspense fallback={<ContentLoadingSpinner text="사원 정보를 불러오는 중..." />}>
<Suspense fallback={<ListPageSkeleton showHeader={false} showStats={true} statsCount={4} />}>
<EmployeeManagement />
</Suspense>
);

View File

@@ -10,7 +10,7 @@
import { Suspense } from 'react';
import { SalaryManagement } from '@/components/hr/SalaryManagement';
import { ContentLoadingSpinner } from '@/components/ui/loading-spinner';
import { ListPageSkeleton } from '@/components/ui/skeleton';
import type { Metadata } from 'next';
/**
@@ -23,7 +23,7 @@ export const metadata: Metadata = {
export default function SalaryManagementPage() {
return (
<Suspense fallback={<ContentLoadingSpinner text="급여 정보를 불러오는 중..." />}>
<Suspense fallback={<ListPageSkeleton showHeader={false} showStats={true} statsCount={4} />}>
<SalaryManagement />
</Suspense>
);

View File

@@ -10,7 +10,7 @@
import { Suspense } from 'react';
import { VacationManagement } from '@/components/hr/VacationManagement';
import { ContentLoadingSpinner } from '@/components/ui/loading-spinner';
import { ListPageSkeleton } from '@/components/ui/skeleton';
import type { Metadata } from 'next';
/**
@@ -23,7 +23,7 @@ export const metadata: Metadata = {
export default function VacationManagementPage() {
return (
<Suspense fallback={<ContentLoadingSpinner text="휴가 정보를 불러오는 중..." />}>
<Suspense fallback={<ListPageSkeleton showHeader={false} showStats={true} statsCount={4} />}>
<VacationManagement />
</Suspense>
);

View File

@@ -1,4 +1,4 @@
import { PageLoadingSpinner } from '@/components/ui/loading-spinner';
import { ListPageSkeleton } from '@/components/ui/skeleton';
/**
* Protected Group Loading UI
@@ -7,11 +7,8 @@ import { PageLoadingSpinner } from '@/components/ui/loading-spinner';
* - AuthenticatedLayout 내에서 표시됨 (사이드바, 헤더 유지)
* - React Suspense 자동 적용
* - 페이지 전환 시 즉각적인 피드백
* - 공통 레이아웃 스타일로 통일 (min-h-[calc(100vh-200px)])
*
* Note: 특정 경로에서 Skeleton UI를 사용하려면 해당 경로에
* 별도의 loading.tsx를 생성하세요. (예: settings/accounts/loading.tsx)
* - 스켈레톤 UI로 레이아웃 유지하며 로딩 표시
*/
export default function ProtectedLoading() {
return <PageLoadingSpinner />;
return <ListPageSkeleton />;
}

View File

@@ -7,7 +7,7 @@
import { Suspense } from 'react';
import { ItemMasterDataManagement } from '@/components/items/ItemMasterDataManagement';
import { ContentLoadingSpinner } from '@/components/ui/loading-spinner';
import { ListPageSkeleton } from '@/components/ui/skeleton';
import type { Metadata } from 'next';
/**
@@ -20,7 +20,7 @@ export const metadata: Metadata = {
export default function ItemMasterDataManagementPage() {
return (
<Suspense fallback={<ContentLoadingSpinner text="품목기준정보를 불러오는 중..." />}>
<Suspense fallback={<ListPageSkeleton showHeader={false} />}>
<ItemMasterDataManagement />
</Suspense>
);

View File

@@ -9,7 +9,7 @@
import { use, useEffect } from 'react';
import { useRouter } from 'next/navigation';
import { ContentLoadingSpinner } from '@/components/ui/loading-spinner';
import { Skeleton } from '@/components/ui/skeleton';
export default function ProcessEditRedirectPage({
params,
@@ -23,5 +23,9 @@ export default function ProcessEditRedirectPage({
router.replace(`/ko/master-data/process-management/${id}?mode=edit`);
}, [router, id]);
return <ContentLoadingSpinner text="페이지 이동 중..." />;
return (
<div className="flex items-center justify-center h-64">
<Skeleton className="h-8 w-48" />
</div>
);
}

View File

@@ -4,7 +4,7 @@
import { Suspense } from 'react';
import ProcessListClient from '@/components/process-management/ProcessListClient';
import { ContentLoadingSpinner } from '@/components/ui/loading-spinner';
import { ListPageSkeleton } from '@/components/ui/skeleton';
import type { Metadata } from 'next';
export const metadata: Metadata = {
@@ -14,7 +14,7 @@ export const metadata: Metadata = {
export default function ProcessManagementPage() {
return (
<Suspense fallback={<ContentLoadingSpinner text="공정 목록을 불러오는 중..." />}>
<Suspense fallback={<ListPageSkeleton showHeader={false} />}>
<ProcessListClient />
</Suspense>
);

View File

@@ -6,11 +6,11 @@
import { Suspense } from 'react';
import ProductionDashboard from '@/components/production/ProductionDashboard';
import { ContentLoadingSpinner } from '@/components/ui/loading-spinner';
import { ListPageSkeleton } from '@/components/ui/skeleton';
export default function ProductionDashboardPage() {
return (
<Suspense fallback={<ContentLoadingSpinner text="생산 현황을 불러오는 중..." />}>
<Suspense fallback={<ListPageSkeleton showHeader={false} showStats={true} statsCount={6} />}>
<ProductionDashboard />
</Suspense>
);

View File

@@ -6,11 +6,11 @@
import { Suspense } from 'react';
import WorkerScreen from '@/components/production/WorkerScreen';
import { ContentLoadingSpinner } from '@/components/ui/loading-spinner';
import { ListPageSkeleton } from '@/components/ui/skeleton';
export default function WorkerScreenPage() {
return (
<Suspense fallback={<ContentLoadingSpinner text="작업자 화면을 불러오는 중..." />}>
<Suspense fallback={<ListPageSkeleton showHeader={false} showStats={true} statsCount={4} />}>
<WorkerScreen />
</Suspense>
);

View File

@@ -9,7 +9,7 @@
import { useEffect } from 'react';
import { useParams, useRouter } from 'next/navigation';
import { ContentLoadingSpinner } from '@/components/ui/loading-spinner';
import { Skeleton } from '@/components/ui/skeleton';
export default function ClientEditRedirectPage() {
const router = useRouter();
@@ -20,5 +20,9 @@ export default function ClientEditRedirectPage() {
router.replace(`/ko/sales/client-management-sales-admin/${id}?mode=edit`);
}, [router, id]);
return <ContentLoadingSpinner text="페이지 이동 중..." />;
return (
<div className="flex items-center justify-center h-64">
<Skeleton className="h-8 w-48" />
</div>
);
}

View File

@@ -49,7 +49,7 @@ import {
ChevronDown,
ChevronUp,
} from "lucide-react";
import { ContentLoadingSpinner } from "@/components/ui/loading-spinner";
import { DetailPageSkeleton } from "@/components/ui/skeleton";
export default function QuoteDetailPage() {
const router = useRouter();
@@ -274,7 +274,7 @@ export default function QuoteDetailPage() {
}, 0) || 0;
if (isLoading) {
return <ContentLoadingSpinner text="견적 정보를 불러오는 중..." />;
return <DetailPageSkeleton sections={2} fieldsPerSection={6} />;
}
if (!quote) {

View File

@@ -9,7 +9,7 @@
import { useEffect } from 'react';
import { useParams, useRouter } from 'next/navigation';
import { ContentLoadingSpinner } from '@/components/ui/loading-spinner';
import { Skeleton } from '@/components/ui/skeleton';
export default function PopupEditRedirectPage() {
const router = useRouter();
@@ -20,5 +20,9 @@ export default function PopupEditRedirectPage() {
router.replace(`/ko/settings/popup-management/${id}?mode=edit`);
}, [router, id]);
return <ContentLoadingSpinner text="페이지 이동 중..." />;
return (
<div className="flex items-center justify-center h-64">
<Skeleton className="h-8 w-48" />
</div>
);
}

View File

@@ -12,7 +12,7 @@ import { useRouter, useSearchParams } from 'next/navigation';
import { BadDebtDetail } from './BadDebtDetail';
import { getBadDebtById } from './actions';
import type { BadDebtRecord } from './types';
import { ContentLoadingSpinner } from '@/components/ui/loading-spinner';
import { DetailPageSkeleton } from '@/components/ui/skeleton';
import { ErrorCard } from '@/components/ui/error-card';
import { toast } from 'sonner';
@@ -86,7 +86,7 @@ export function BadDebtDetailClientV2({ recordId, initialMode }: BadDebtDetailCl
// 로딩 중
if (isLoading) {
return <ContentLoadingSpinner text="악성채권 정보를 불러오는 중..." />;
return <DetailPageSkeleton sections={2} fieldsPerSection={6} />;
}
// 에러 발생 (view/edit 모드에서)

View File

@@ -512,6 +512,7 @@ export function BillManagement({ initialVendorId, initialBillType }: BillManagem
itemsPerPage: pagination.perPage,
onPageChange: setCurrentPage,
}}
externalIsLoading={isLoading}
/>
);
}

View File

@@ -601,7 +601,7 @@ export function CardTransactionInquiry({
return (
<>
<UniversalListPage config={config} initialData={filteredData} />
<UniversalListPage config={config} initialData={filteredData} externalIsLoading={isLoading} />
{/* 계정과목명 저장 확인 다이얼로그 */}
<Dialog open={showSaveDialog} onOpenChange={setShowSaveDialog}>

View File

@@ -529,7 +529,7 @@ export function PurchaseManagement() {
return (
<>
<UniversalListPage config={config} initialData={purchaseData} />
<UniversalListPage config={config} initialData={purchaseData} externalIsLoading={isLoading} />
{/* 계정과목명 저장 확인 다이얼로그 */}
<Dialog open={showSaveDialog} onOpenChange={setShowSaveDialog}>

View File

@@ -10,7 +10,7 @@ import { useRouter, useSearchParams } from 'next/navigation';
import { format, startOfMonth, endOfMonth } from 'date-fns';
import { Download, Pencil } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { ContentLoadingSpinner } from '@/components/ui/loading-spinner';
import { ContentSkeleton } from '@/components/ui/skeleton';
import { Card, CardContent } from '@/components/ui/card';
import {
Table,
@@ -155,7 +155,7 @@ export function VendorLedgerDetail({
const renderFormContent = useCallback(() => {
// 로딩 상태 표시
if (isLoading && !vendorDetail) {
return <ContentLoadingSpinner text="거래처 원장을 불러오는 중..." />;
return <ContentSkeleton type="detail" rows={6} />;
}
// 데이터 없음
@@ -272,7 +272,7 @@ export function VendorLedgerDetail({
<Card>
<CardContent className="p-0">
{isLoading ? (
<ContentLoadingSpinner text="거래 내역을 불러오는 중..." />
<ContentSkeleton type="table" rows={5} />
) : transactions.length === 0 ? (
<div className="flex items-center justify-center h-32 text-gray-500">
.

View File

@@ -4,7 +4,6 @@ import { useState, useCallback, useEffect } from 'react';
import { useDaumPostcode } from '@/hooks/useDaumPostcode';
import { useRouter } from 'next/navigation';
import { Plus, X } from 'lucide-react';
import { ContentLoadingSpinner } from '@/components/ui/loading-spinner';
import { toast } from 'sonner';
import { getClientById, createClient, updateClient, deleteClient } from './actions';
import { IntegratedDetailTemplate } from '@/components/templates/IntegratedDetailTemplate';

View File

@@ -810,6 +810,7 @@ export function ApprovalBox() {
onTabChange={handleTabChange}
onSearchChange={setSearchQuery}
onFilterChange={handleMobileFilterChange}
externalIsLoading={isLoading}
/>
);
}

View File

@@ -1,7 +1,7 @@
'use client';
import { Fragment } from 'react';
import { ContentLoadingSpinner } from '@/components/ui/loading-spinner';
import { ContentSkeleton } from '@/components/ui/skeleton';
import { Checkbox } from '@/components/ui/checkbox';
import {
Table,
@@ -63,7 +63,7 @@ export function ExpenseEstimateForm({ data, onChange, isLoading }: ExpenseEstima
<div className="space-y-6">
<div className="bg-white rounded-lg border p-6">
<h3 className="text-lg font-semibold mb-4"> </h3>
<ContentLoadingSpinner text="항목을 불러오는 중..." />
<ContentSkeleton type="table" rows={5} />
</div>
</div>
);

View File

@@ -776,6 +776,7 @@ export function DraftBox() {
}}
onSearchChange={setSearchQuery}
onFilterChange={handleFilterChange}
externalIsLoading={isLoading}
/>
);
}

View File

@@ -671,6 +671,7 @@ export function ReferenceBox() {
onTabChange={handleTabChange}
onSearchChange={setSearchQuery}
onFilterChange={handleMobileFilterChange}
externalIsLoading={isLoading}
/>
);
}

View File

@@ -13,7 +13,6 @@ import { useState, useMemo, useCallback, useEffect } from 'react';
import { useRouter } from 'next/navigation';
import { format } from 'date-fns';
import { FileText, Plus, Pencil, Trash2 } from 'lucide-react';
import { ContentLoadingSpinner } from '@/components/ui/loading-spinner';
import { Button } from '@/components/ui/button';
import { Checkbox } from '@/components/ui/checkbox';
import { TableRow, TableCell } from '@/components/ui/table';
@@ -399,11 +398,6 @@ export function BoardList() {
]
);
// ===== 로딩 상태 =====
if (isLoading && posts.length === 0 && boards.length === 0) {
return <ContentLoadingSpinner text="게시글을 불러오는 중..." />;
}
return (
<UniversalListPage<Post>
config={boardListConfig}
@@ -420,6 +414,7 @@ export function BoardList() {
}}
onTabChange={handleTabChange}
onSearchChange={setSearchQuery}
externalIsLoading={isLoading}
/>
);
}

View File

@@ -15,7 +15,7 @@ import { BoardForm } from './BoardForm';
import { getBoardById, createBoard, updateBoard, deleteBoard } from './actions';
import { forceRefreshMenus } from '@/lib/utils/menuRefresh';
import type { Board, BoardFormData } from './types';
import { ContentLoadingSpinner } from '@/components/ui/loading-spinner';
import { DetailPageSkeleton } from '@/components/ui/skeleton';
import { ErrorCard } from '@/components/ui/error-card';
import { Button } from '@/components/ui/button';
import { DeleteConfirmDialog } from '@/components/ui/confirm-dialog';
@@ -197,7 +197,7 @@ export function BoardDetailClientV2({ boardId, initialMode }: BoardDetailClientV
// 로딩 중
if (isLoading) {
return <ContentLoadingSpinner text="게시판 정보를 불러오는 중..." />;
return <DetailPageSkeleton sections={1} fieldsPerSection={6} />;
}
// 에러 발생 (view/edit 모드에서)

View File

@@ -57,19 +57,19 @@ export function CEODashboard() {
// Welfare API Hook (Phase 2)
const welfareData = useWelfare();
// 전체 로딩 상태 (모든 API 호출 중일 때)
// 전체 로딩 상태 (하나라도 로딩 중이면 스켈레톤 표시)
const isLoading = useMemo(() => {
return (
apiData.dailyReport.loading &&
apiData.receivable.loading &&
apiData.debtCollection.loading &&
apiData.monthlyExpense.loading &&
apiData.cardManagement.loading &&
apiData.statusBoard.loading &&
todayIssueData.loading &&
calendarData.loading &&
vatData.loading &&
entertainmentData.loading &&
apiData.dailyReport.loading ||
apiData.receivable.loading ||
apiData.debtCollection.loading ||
apiData.monthlyExpense.loading ||
apiData.cardManagement.loading ||
apiData.statusBoard.loading ||
todayIssueData.loading ||
calendarData.loading ||
vatData.loading ||
entertainmentData.loading ||
welfareData.loading
);
}, [apiData, todayIssueData.loading, calendarData.loading, vatData.loading, entertainmentData.loading, welfareData.loading]);

View File

@@ -2,7 +2,7 @@
import { Suspense } from "react";
import { CEODashboard } from "./CEODashboard";
import { PageLoadingSpinner } from "@/components/ui/loading-spinner";
import { DetailPageSkeleton } from "@/components/ui/skeleton";
/**
* Dashboard - 대표님 전용 대시보드
@@ -26,7 +26,7 @@ import { PageLoadingSpinner } from "@/components/ui/loading-spinner";
export function Dashboard() {
console.log('🎨 CEO Dashboard component rendering...');
return (
<Suspense fallback={<PageLoadingSpinner text="대시보드를 불러오는 중..." />}>
<Suspense fallback={<DetailPageSkeleton />}>
<CEODashboard />
</Suspense>
);

View File

@@ -2,7 +2,7 @@
import { Suspense } from "react";
import { ConstructionMainDashboard } from "./ConstructionMainDashboard";
import { PageLoadingSpinner } from "@/components/ui/loading-spinner";
import { DetailPageSkeleton } from "@/components/ui/skeleton";
/**
* ConstructionDashboard - 주일기업 전용 대시보드
@@ -12,7 +12,7 @@ import { PageLoadingSpinner } from "@/components/ui/loading-spinner";
export function ConstructionDashboard() {
console.log('🏗️ Construction Dashboard rendering...');
return (
<Suspense fallback={<PageLoadingSpinner text="공사 현황을 불러오는 중..." />}>
<Suspense fallback={<DetailPageSkeleton />}>
<ConstructionMainDashboard />
</Suspense>
);

View File

@@ -4,7 +4,7 @@ import { useState, useEffect, useCallback } from 'react';
import { PageLayout } from '@/components/organisms/PageLayout';
import { PageHeader } from '@/components/organisms/PageHeader';
import { FolderTree, Plus, GripVertical, Pencil, Trash2, Loader2 } from 'lucide-react';
import { ContentLoadingSpinner } from '@/components/ui/loading-spinner';
import { ContentSkeleton } from '@/components/ui/skeleton';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Card, CardContent } from '@/components/ui/card';
@@ -253,7 +253,7 @@ export function CategoryManagement() {
<Card>
<CardContent className="p-0">
{isLoading ? (
<ContentLoadingSpinner text="카테고리 목록을 불러오는 중..." />
<ContentSkeleton type="list" rows={5} />
) : (
<div className="divide-y">
{categories.map((category, index) => (

View File

@@ -14,7 +14,7 @@ import { useState, useEffect } from 'react';
import { useRouter, useSearchParams } from 'next/navigation';
import SiteDetailForm from './SiteDetailForm';
import type { Site } from './types';
import { ContentLoadingSpinner } from '@/components/ui/loading-spinner';
import { DetailPageSkeleton } from '@/components/ui/skeleton';
import { ErrorCard } from '@/components/ui/error-card';
type DetailMode = 'view' | 'edit';
@@ -104,7 +104,7 @@ export function SiteDetailClientV2({ siteId, initialMode }: SiteDetailClientV2Pr
// ===== 로딩 상태 =====
if (isLoading) {
return <ContentLoadingSpinner />;
return <DetailPageSkeleton sections={2} fieldsPerSection={6} />;
}
// ===== 에러 상태 =====

View File

@@ -14,7 +14,7 @@ import { useState, useEffect } from 'react';
import { useRouter, useSearchParams } from 'next/navigation';
import StructureReviewDetailForm from './StructureReviewDetailForm';
import type { StructureReview } from './types';
import { ContentLoadingSpinner } from '@/components/ui/loading-spinner';
import { DetailPageSkeleton } from '@/components/ui/skeleton';
import { ErrorCard } from '@/components/ui/error-card';
type DetailMode = 'view' | 'edit';
@@ -112,7 +112,7 @@ export function StructureReviewDetailClientV2({
// ===== 로딩 상태 =====
if (isLoading) {
return <ContentLoadingSpinner />;
return <DetailPageSkeleton sections={2} fieldsPerSection={6} />;
}
// ===== 에러 상태 =====

View File

@@ -25,7 +25,7 @@ import {
deletePost,
} from '../shared/actions';
import { transformApiToComment } from '../shared/types';
import { ContentLoadingSpinner } from '@/components/ui/loading-spinner';
import { DetailPageSkeleton } from '@/components/ui/skeleton';
import { ErrorCard } from '@/components/ui/error-card';
type DetailMode = 'view' | 'edit' | 'create';
@@ -165,7 +165,7 @@ export function InquiryDetailClientV2({ inquiryId, initialMode }: InquiryDetailC
// ===== 로딩 상태 =====
if (isLoading) {
return <ContentLoadingSpinner />;
return <DetailPageSkeleton sections={1} fieldsPerSection={4} />;
}
// ===== 에러 상태 =====

View File

@@ -12,7 +12,6 @@ import {
FileText,
Edit,
} from 'lucide-react';
import { ContentLoadingSpinner } from '@/components/ui/loading-spinner';
import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
import { Checkbox } from '@/components/ui/checkbox';
@@ -673,16 +672,12 @@ export function AttendanceManagement() {
handleSubmitReason,
]);
// 로딩 상태
if (isLoading) {
return <ContentLoadingSpinner text="근태 정보를 불러오는 중..." />;
}
return (
<UniversalListPage<AttendanceRecord>
config={attendanceConfig}
initialData={mergedRecords}
initialTotalCount={mergedRecords.length}
externalIsLoading={isLoading}
/>
);
}

View File

@@ -405,6 +405,7 @@ export function CardManagement({ initialData }: CardManagementProps) {
config={cardManagementConfig}
initialData={cards}
initialTotalCount={cards.length}
externalIsLoading={isLoading}
/>
);
}

View File

@@ -764,6 +764,7 @@ export function EmployeeManagement() {
config={employeeConfig}
initialData={employees}
initialTotalCount={employees.length}
externalIsLoading={isLoading}
/>
);
}

View File

@@ -553,6 +553,7 @@ export function SalaryManagement() {
getItemId: (item) => item.id,
}}
onSearchChange={setSearchQuery}
externalIsLoading={isLoading}
/>
);
}

View File

@@ -837,6 +837,7 @@ export function VacationManagement() {
onTabChange={handleMainTabChange}
onSearchChange={setSearchQuery}
onFilterChange={handleFilterChange}
externalIsLoading={isLoading}
/>
);
}

View File

@@ -11,7 +11,7 @@ import { useRouter } from 'next/navigation';
import { cn } from '@/lib/utils';
import { Label } from '@/components/ui/label';
import { Input } from '@/components/ui/input';
import { PageLoadingSpinner } from '@/components/ui/loading-spinner';
import { FormSectionSkeleton } from '@/components/ui/skeleton';
import { Alert, AlertDescription } from '@/components/ui/alert';
import {
Card,
@@ -616,12 +616,7 @@ export default function DynamicItemForm({
// 로딩 상태
if (isLoading && selectedItemType) {
return (
<PageLoadingSpinner
text="폼 구조를 불러오는 중..."
minHeight="min-h-[40vh]"
/>
);
return <FormSectionSkeleton />;
}
// 에러 상태

View File

@@ -13,7 +13,7 @@ import { useRouter } from 'next/navigation';
import DynamicItemForm from '@/components/items/DynamicItemForm';
import type { DynamicFormData, BOMLine } from '@/components/items/DynamicItemForm/types';
import type { ItemType } from '@/types/item';
import { ContentLoadingSpinner } from '@/components/ui/loading-spinner';
import { DetailPageSkeleton } from '@/components/ui/skeleton';
import {
isMaterialType,
transformMaterialDataForSave,
@@ -368,7 +368,7 @@ export function ItemDetailEdit({ itemCode, itemType: urlItemType, itemId: urlIte
// 로딩 상태
if (isLoading) {
return <ContentLoadingSpinner text="품목 정보를 불러오는 중..." />;
return <DetailPageSkeleton sections={2} fieldsPerSection={6} />;
}
// 에러 상태

View File

@@ -11,7 +11,7 @@ import { useRouter } from 'next/navigation';
import { notFound } from 'next/navigation';
import ItemDetailClient from '@/components/items/ItemDetailClient';
import type { ItemMaster, ItemType, ProductCategory, PartType, PartUsage } from '@/types/item';
import { ContentLoadingSpinner } from '@/components/ui/loading-spinner';
import { DetailPageSkeleton } from '@/components/ui/skeleton';
import { ServerErrorPage } from '@/components/common/ServerErrorPage';
// Materials 타입 (SM, RM, CS는 Material 테이블 사용)
@@ -253,7 +253,7 @@ export function ItemDetailView({ itemCode, itemType, itemId }: ItemDetailViewPro
// 로딩 상태
if (isLoading) {
return <ContentLoadingSpinner text="품목 정보를 불러오는 중..." />;
return <DetailPageSkeleton sections={2} fieldsPerSection={6} />;
}
// 에러 상태

View File

@@ -18,7 +18,6 @@ import { Checkbox } from '@/components/ui/checkbox';
import { TableRow, TableCell } from '@/components/ui/table';
import { DeleteConfirmDialog } from '@/components/ui/confirm-dialog';
import { Search, Plus, Edit, Trash2, Package } from 'lucide-react';
import { TableLoadingSpinner } from '@/components/ui/loading-spinner';
import { useItemList } from '@/hooks/useItemList';
import { handleApiError } from '@/lib/api/error-handler';
import { isNextRedirectError } from '@/lib/utils/redirect-error';
@@ -122,11 +121,6 @@ export default function ItemListClient() {
});
}, [debouncedSearchTerm, selectedType, search]);
// 로딩 상태
if (isLoading) {
return <TableLoadingSpinner text="품목 목록을 불러오는 중..." />;
}
// 유형 변경 핸들러
const handleTypeChange = (value: string) => {
setSelectedType(value);
@@ -491,6 +485,7 @@ export default function ItemListClient() {
itemsPerPage: pagination.perPage,
onPageChange: handlePageChange,
}}
externalIsLoading={isLoading}
/>
{/* 개별 삭제 확인 다이얼로그 */}

View File

@@ -6,7 +6,7 @@ import { PageHeader } from '@/components/organisms/PageHeader';
import { useItemMaster } from '@/contexts/ItemMasterContext';
import type { SectionTemplate, BOMItem, TemplateField } from '@/contexts/ItemMasterContext';
import { MasterFieldTab, HierarchyTab, SectionsTab } from './ItemMasterDataManagement/tabs';
import { LoadingSpinner } from '@/components/ui/loading-spinner';
import { DetailPageSkeleton } from '@/components/ui/skeleton';
import { ServerErrorPage } from '@/components/common/ServerErrorPage';
// 2025-12-24: Phase 2 UI 컴포넌트 분리
import { AttributeTabContent } from './ItemMasterDataManagement/components';
@@ -450,11 +450,7 @@ function ItemMasterDataManagementContent() {
// 초기 로딩 중 UI
if (isInitialLoading) {
return (
<div className="flex items-center justify-center min-h-screen">
<LoadingSpinner size="lg" text="데이터를 불러오는 중..." />
</div>
);
return <DetailPageSkeleton />;
}
// 에러 발생 시 UI

View File

@@ -16,7 +16,7 @@ import { Badge } from '@/components/ui/badge';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { FormInput, Search, Info, Loader2, Hash, Calendar, CheckSquare, ChevronDown, Type, AlignLeft, Database } from 'lucide-react';
import { ContentLoadingSpinner } from '@/components/ui/loading-spinner';
import { ContentSkeleton } from '@/components/ui/skeleton';
import type { ItemField, ItemMasterField } from '@/contexts/ItemMasterContext';
import type { FieldUsageResponse } from '@/types/item-master-api';
import { isNextRedirectError } from '@/lib/utils/redirect-error';
@@ -157,7 +157,7 @@ export function ImportFieldDialog({
<div className="space-y-4">
{isLoading ? (
<ContentLoadingSpinner text="필드 목록을 불러오는 중..." />
<ContentSkeleton type="list" rows={5} />
) : filteredFields.length === 0 ? (
<div className="text-center py-8">
<FormInput className="h-12 w-12 mx-auto text-muted-foreground mb-4" />

View File

@@ -7,7 +7,7 @@ import { Badge } from '@/components/ui/badge';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Package, Folder, Search, Info, Loader2 } from 'lucide-react';
import { ContentLoadingSpinner } from '@/components/ui/loading-spinner';
import { ContentSkeleton } from '@/components/ui/skeleton';
import type { ItemSection } from '@/contexts/ItemMasterContext';
import type { SectionUsageResponse } from '@/types/item-master-api';
import { isNextRedirectError } from '@/lib/utils/redirect-error';
@@ -109,7 +109,7 @@ export function ImportSectionDialog({
<div className="space-y-4">
{isLoading ? (
<ContentLoadingSpinner text="섹션 목록을 불러오는 중..." />
<ContentSkeleton type="list" rows={5} />
) : filteredSections.length === 0 ? (
<div className="text-center py-8">
<Folder className="h-12 w-12 mx-auto text-muted-foreground mb-4" />

View File

@@ -15,7 +15,7 @@ import { useRouter } from 'next/navigation';
import { Calendar } from 'lucide-react';
import { IntegratedDetailTemplate } from '@/components/templates/IntegratedDetailTemplate';
import { materialInspectionCreateConfig } from './inspectionConfig';
import { ContentLoadingSpinner } from '@/components/ui/loading-spinner';
import { ContentSkeleton } from '@/components/ui/skeleton';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
@@ -195,7 +195,7 @@ export function InspectionCreate({ id }: Props) {
<Label className="text-sm font-medium"> </Label>
<div className="space-y-2 border rounded-lg p-2 bg-white min-h-[200px]">
{isLoadingTargets ? (
<ContentLoadingSpinner text="검사 대상을 불러오는 중..." />
<ContentSkeleton type="list" rows={3} />
) : inspectionTargets.length === 0 ? (
<div className="text-center py-8 text-muted-foreground text-sm">
.

View File

@@ -13,7 +13,7 @@ import { ProcessDetail } from './ProcessDetail';
import { ProcessForm } from './ProcessForm';
import { getProcessById } from './actions';
import type { Process } from '@/types/process';
import { ContentLoadingSpinner } from '@/components/ui/loading-spinner';
import { DetailPageSkeleton } from '@/components/ui/skeleton';
import { ErrorCard } from '@/components/ui/error-card';
import { toast } from 'sonner';
@@ -87,7 +87,7 @@ export function ProcessDetailClientV2({ processId, initialMode }: ProcessDetailC
// 로딩 중
if (isLoading) {
return <ContentLoadingSpinner text="공정 정보를 불러오는 중..." />;
return <DetailPageSkeleton sections={1} fieldsPerSection={4} />;
}
// 에러 발생 (view/edit 모드에서)

View File

@@ -13,7 +13,7 @@
import { useState, useMemo, useCallback, useEffect } from 'react';
import { useRouter } from 'next/navigation';
import { Factory, Clock, PlayCircle, CheckCircle2, AlertTriangle, Timer, Users } from 'lucide-react';
import { ContentLoadingSpinner } from '@/components/ui/loading-spinner';
import { StatCardGridSkeleton } from '@/components/ui/skeleton';
import { Tabs, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Badge } from '@/components/ui/badge';
@@ -170,7 +170,7 @@ export default function ProductionDashboard() {
</Tabs>
{/* 로딩 상태 */}
{isLoading && <ContentLoadingSpinner text="생산 현황을 불러오는 중..." />}
{isLoading && <StatCardGridSkeleton count={6} />}
{/* 통계 카드 */}
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-6 gap-4">

View File

@@ -9,7 +9,7 @@
import { useState, useEffect, useMemo } from 'react';
import { Check, X, ChevronDown, ChevronRight } from 'lucide-react';
import { ContentLoadingSpinner } from '@/components/ui/loading-spinner';
import { ContentSkeleton } from '@/components/ui/skeleton';
import { Button } from '@/components/ui/button';
import { Checkbox } from '@/components/ui/checkbox';
import {
@@ -290,7 +290,7 @@ export function AssigneeSelectModal({
{/* 컨텐츠 영역 */}
<div className="max-h-[400px] overflow-y-auto">
{loading ? (
<ContentLoadingSpinner text="부서 목록을 불러오는 중..." />
<ContentSkeleton type="list" rows={6} />
) : error ? (
<div className="flex flex-col items-center justify-center py-12 text-center">
<p className="text-red-500 mb-2">{error}</p>

View File

@@ -7,7 +7,7 @@
import { useState, useEffect, useCallback } from 'react';
import { Search, FileText } from 'lucide-react';
import { ContentLoadingSpinner } from '@/components/ui/loading-spinner';
import { ContentSkeleton } from '@/components/ui/skeleton';
import {
Dialog,
DialogContent,
@@ -121,7 +121,7 @@ export function SalesOrderSelectModal({
{/* 수주 목록 */}
<div className="max-h-[400px] overflow-y-auto space-y-2">
{isLoading ? (
<ContentLoadingSpinner text="수주 목록을 불러오는 중..." />
<ContentSkeleton type="cards" rows={4} />
) : salesOrders.map((order) => (
<div
key={order.id}

View File

@@ -513,6 +513,7 @@ export function WorkOrderCreate() {
<IntegratedDetailTemplate
config={workOrderCreateConfig}
mode="create"
isLoading={isLoadingProcesses}
isSubmitting={isSubmitting}
onBack={handleCancel}
onCancel={handleCancel}

View File

@@ -12,7 +12,7 @@
import { useState, useEffect, useCallback } from 'react';
import { Loader2 } from 'lucide-react';
import { ContentLoadingSpinner } from '@/components/ui/loading-spinner';
import { ContentSkeleton } from '@/components/ui/skeleton';
import {
Dialog,
DialogContent,
@@ -200,7 +200,7 @@ export function MaterialInputModal({
</h3>
{isLoading ? (
<ContentLoadingSpinner text="자재 목록을 불러오는 중..." />
<ContentSkeleton type="table" rows={4} />
) : materials.length === 0 ? (
<div className="border rounded-lg">
<Table>

View File

@@ -14,7 +14,7 @@
import { useState, useEffect, useCallback } from 'react';
import { ChevronDown, Loader2 } from 'lucide-react';
import { ContentLoadingSpinner } from '@/components/ui/loading-spinner';
import { ContentSkeleton } from '@/components/ui/skeleton';
import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
import {
@@ -176,7 +176,7 @@ export function ProcessDetailSection({
{/* 공정 단계 목록 */}
<div className="space-y-2">
{isLoading ? (
<ContentLoadingSpinner text="공정 단계를 불러오는 중..." />
<ContentSkeleton type="list" rows={4} />
) : steps.length === 0 ? (
<div className="py-8 text-center text-gray-500">
.

View File

@@ -12,7 +12,7 @@
import { useState, useMemo, useCallback, useEffect } from 'react';
import { ClipboardList, PlayCircle, CheckCircle2, AlertTriangle } from 'lucide-react';
import { ContentLoadingSpinner } from '@/components/ui/loading-spinner';
import { ContentSkeleton } from '@/components/ui/skeleton';
import { Card, CardContent } from '@/components/ui/card';
import {
Select,
@@ -281,7 +281,7 @@ export default function WorkerScreen() {
</Select>
</div>
{isLoading ? (
<ContentLoadingSpinner text="작업 목록을 불러오는 중..." />
<ContentSkeleton type="cards" rows={4} />
) : sortedWorkOrders.length === 0 ? (
<Card>
<CardContent className="py-12 text-center text-muted-foreground">

View File

@@ -9,7 +9,6 @@
import { useState, useCallback, useEffect, useMemo } from 'react';
import { useRouter, useSearchParams } from 'next/navigation';
import { Printer, Paperclip, Loader2 } from 'lucide-react';
import { ContentLoadingSpinner } from '@/components/ui/loading-spinner';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';

View File

@@ -22,7 +22,7 @@ import {
import { Textarea } from '@/components/ui/textarea';
import { Label } from '@/components/ui/label';
import { Check, AlertTriangle, Info, AlertCircle, BarChart3, Loader2, RefreshCw } from 'lucide-react';
import { ContentLoadingSpinner } from '@/components/ui/loading-spinner';
import { DetailPageSkeleton } from '@/components/ui/skeleton';
import { cn } from '@/lib/utils';
import { toast } from 'sonner';
import { PageLayout } from '@/components/organisms/PageLayout';
@@ -381,7 +381,7 @@ export default function ComprehensiveAnalysis({ initialData }: ComprehensiveAnal
description="종합 경영 분석 현황을 확인합니다."
icon={BarChart3}
/>
<ContentLoadingSpinner text="데이터를 불러오는 중..." />
<DetailPageSkeleton />
</PageLayout>
);
}

View File

@@ -5,7 +5,7 @@ import { PageLayout } from '@/components/organisms/PageLayout';
import { PageHeader } from '@/components/organisms/PageHeader';
import { CalendarDays, Loader2 } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { ContentLoadingSpinner } from '@/components/ui/loading-spinner';
import { ContentSkeleton } from '@/components/ui/skeleton';
import { Label } from '@/components/ui/label';
import { Input } from '@/components/ui/input';
import { QuantityInput } from '@/components/ui/quantity-input';
@@ -92,7 +92,7 @@ export function LeavePolicyManagement() {
description="휴가 정책을 관리합니다"
icon={CalendarDays}
/>
<ContentLoadingSpinner text="휴가 정책을 불러오는 중..." />
<ContentSkeleton type="form" rows={6} />
</PageLayout>
);
}

View File

@@ -14,7 +14,7 @@ import {
RotateCcw,
Loader2,
} from 'lucide-react';
import { ContentLoadingSpinner } from '@/components/ui/loading-spinner';
import { DetailPageSkeleton } from '@/components/ui/skeleton';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
@@ -456,7 +456,7 @@ export function PermissionDetailClient({ permissionId, isNew = false }: Permissi
if (isLoading) {
return (
<PageLayout>
<ContentLoadingSpinner text="권한 정보를 불러오는 중..." />
<DetailPageSkeleton />
</PageLayout>
);
}

View File

@@ -15,7 +15,6 @@ import {
Loader2,
} from 'lucide-react';
import { Button } from '@/components/ui/button';
import { ContentLoadingSpinner } from '@/components/ui/loading-spinner';
import { Checkbox } from '@/components/ui/checkbox';
import { Badge } from '@/components/ui/badge';
import { TableRow, TableCell } from '@/components/ui/table';
@@ -394,11 +393,7 @@ export function PermissionManagement() {
</div>
), [handleBulkDelete, handleAdd]);
// ===== 로딩/에러 상태 =====
if (isLoading) {
return <ContentLoadingSpinner text="권한 정보를 불러오는 중..." />;
}
// ===== 에러 상태 =====
if (error) {
return (
<div className="flex flex-col items-center justify-center h-96 gap-4">
@@ -517,6 +512,7 @@ export function PermissionManagement() {
searchValue: searchQuery,
setSearchValue: setSearchQuery,
}}
externalIsLoading={isLoading}
/>
);
}

View File

@@ -4,7 +4,7 @@ import { useState, useEffect, useCallback } from 'react';
import { PageLayout } from '@/components/organisms/PageLayout';
import { PageHeader } from '@/components/organisms/PageHeader';
import { Award, Plus, GripVertical, Pencil, Trash2, Loader2 } from 'lucide-react';
import { ContentLoadingSpinner } from '@/components/ui/loading-spinner';
import { ContentSkeleton } from '@/components/ui/skeleton';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Card, CardContent } from '@/components/ui/card';
@@ -252,7 +252,7 @@ export function RankManagement() {
<Card>
<CardContent className="p-0">
{isLoading ? (
<ContentLoadingSpinner text="직급 목록을 불러오는 중..." />
<ContentSkeleton type="list" rows={5} />
) : (
<div className="divide-y">
{ranks.map((rank, index) => (

View File

@@ -4,7 +4,7 @@ import { useState, useEffect, useCallback } from 'react';
import { PageLayout } from '@/components/organisms/PageLayout';
import { PageHeader } from '@/components/organisms/PageHeader';
import { Briefcase, Plus, GripVertical, Pencil, Trash2, Loader2 } from 'lucide-react';
import { ContentLoadingSpinner } from '@/components/ui/loading-spinner';
import { ContentSkeleton } from '@/components/ui/skeleton';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Card, CardContent } from '@/components/ui/card';
@@ -252,7 +252,7 @@ export function TitleManagement() {
<Card>
<CardContent className="p-0">
{isLoading ? (
<ContentLoadingSpinner text="직책 목록을 불러오는 중..." />
<ContentSkeleton type="list" rows={5} />
) : (
<div className="divide-y">
{titles.map((title, index) => (

View File

@@ -34,6 +34,7 @@ export function UniversalListPage<T>({
onTabChange,
onSearchChange,
onFilterChange: onFilterChangeCallback,
externalIsLoading,
}: UniversalListPageProps<T>) {
const router = useRouter();
const params = useParams();
@@ -593,8 +594,8 @@ export function UniversalListPage<T>({
renderMobileCard={renderMobileCard}
// 페이지네이션
pagination={paginationConfig}
// 로딩 상태
isLoading={isLoading}
// 로딩 상태 (외부 로딩 상태 우선 사용)
isLoading={externalIsLoading ?? isLoading}
/>
{/* 삭제 확인 다이얼로그 */}

View File

@@ -377,6 +377,13 @@ export interface UniversalListPageProps<T> {
* - 외부에서 API 호출 후 데이터 갱신 가능
*/
onFilterChange?: (filters: Record<string, string | string[]>) => void;
/**
* 외부 로딩 상태 (서버 사이드 데이터 페칭용)
* - 설정 시 내부 isLoading 대신 사용
* - 부모 컴포넌트에서 데이터 로딩 상태를 직접 관리할 때 사용
* - IntegratedListTemplateV2로 전달되어 스켈레톤 표시에 사용
*/
externalIsLoading?: boolean;
}
// ===== 내부 상태 타입 =====

View File

@@ -451,7 +451,95 @@ function ListPageSkeleton({
}
// ============================================
// 12. 페이지 헤더 스켈레톤
// 12. 콘텐츠 스켈레톤 (범용 - ContentLoadingSpinner 대체용)
// ============================================
interface ContentSkeletonProps {
/** 스켈레톤 유형 */
type?: 'list' | 'detail' | 'form' | 'table' | 'cards';
/** 행/카드 개수 (default: 5) */
rows?: number;
/** 메시지 표시 (deprecated - 스켈레톤은 메시지 없음) */
text?: string;
}
function ContentSkeleton({
type = 'list',
rows = 5,
}: ContentSkeletonProps) {
switch (type) {
case 'detail':
return (
<div className="p-6 space-y-4 animate-pulse">
{/* 상세 정보 그리드 */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{Array.from({ length: rows }).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>
</div>
);
case 'form':
return (
<div className="p-6 space-y-4 animate-pulse">
{Array.from({ length: rows }).map((_, i) => (
<div key={i} className="space-y-2">
<Skeleton className="h-4 w-24" />
<Skeleton className="h-10 w-full rounded-md" />
</div>
))}
</div>
);
case 'table':
return (
<div className="p-4 animate-pulse">
<div className="space-y-3">
{Array.from({ length: rows }).map((_, i) => (
<div key={i} className="flex items-center gap-4">
<Skeleton className="h-4 w-4 rounded" />
<Skeleton className="h-4 flex-1" />
<Skeleton className="h-4 w-20" />
</div>
))}
</div>
</div>
);
case 'cards':
return (
<div className="p-4 space-y-3 animate-pulse">
{Array.from({ length: rows }).map((_, i) => (
<div key={i} className="p-4 border rounded-lg space-y-2">
<div className="flex items-center justify-between">
<Skeleton className="h-5 w-32" />
<Skeleton className="h-5 w-16 rounded-full" />
</div>
<Skeleton className="h-4 w-24" />
<Skeleton className="h-4 w-full" />
</div>
))}
</div>
);
case 'list':
default:
return (
<div className="p-4 space-y-3 animate-pulse">
{Array.from({ length: rows }).map((_, i) => (
<div key={i} className="flex items-center gap-3 py-2">
<Skeleton className="h-5 w-5 rounded" />
<Skeleton className="h-4 w-8" />
<Skeleton className="h-4 flex-1" />
<Skeleton className="h-8 w-8 rounded" />
</div>
))}
</div>
);
}
}
// ============================================
// 13. 페이지 헤더 스켈레톤
// ============================================
interface PageHeaderSkeletonProps {
showActions?: boolean;
@@ -498,4 +586,5 @@ export {
StatCardGridSkeleton,
ListPageSkeleton,
PageHeaderSkeleton,
ContentSkeleton,
};

File diff suppressed because one or more lines are too long