refactor: UniversalListPage externalIsLoading 지원 및 스켈레톤 개선
- UniversalListPage에 externalIsLoading prop 추가 - CardTransactionDetailClient DevFill 자동입력 기능 추가 - 여러 컴포넌트 로딩 상태 처리 개선 - skeleton 컴포넌트 확장 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -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) {
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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} />;
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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 />;
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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 모드에서)
|
||||
|
||||
@@ -512,6 +512,7 @@ export function BillManagement({ initialVendorId, initialBillType }: BillManagem
|
||||
itemsPerPage: pagination.perPage,
|
||||
onPageChange: setCurrentPage,
|
||||
}}
|
||||
externalIsLoading={isLoading}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -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}>
|
||||
|
||||
@@ -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}>
|
||||
|
||||
@@ -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">
|
||||
거래 내역이 없습니다.
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -810,6 +810,7 @@ export function ApprovalBox() {
|
||||
onTabChange={handleTabChange}
|
||||
onSearchChange={setSearchQuery}
|
||||
onFilterChange={handleMobileFilterChange}
|
||||
externalIsLoading={isLoading}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -776,6 +776,7 @@ export function DraftBox() {
|
||||
}}
|
||||
onSearchChange={setSearchQuery}
|
||||
onFilterChange={handleFilterChange}
|
||||
externalIsLoading={isLoading}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -671,6 +671,7 @@ export function ReferenceBox() {
|
||||
onTabChange={handleTabChange}
|
||||
onSearchChange={setSearchQuery}
|
||||
onFilterChange={handleMobileFilterChange}
|
||||
externalIsLoading={isLoading}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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 모드에서)
|
||||
|
||||
@@ -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]);
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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) => (
|
||||
|
||||
@@ -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} />;
|
||||
}
|
||||
|
||||
// ===== 에러 상태 =====
|
||||
|
||||
@@ -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} />;
|
||||
}
|
||||
|
||||
// ===== 에러 상태 =====
|
||||
|
||||
@@ -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} />;
|
||||
}
|
||||
|
||||
// ===== 에러 상태 =====
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -405,6 +405,7 @@ export function CardManagement({ initialData }: CardManagementProps) {
|
||||
config={cardManagementConfig}
|
||||
initialData={cards}
|
||||
initialTotalCount={cards.length}
|
||||
externalIsLoading={isLoading}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -764,6 +764,7 @@ export function EmployeeManagement() {
|
||||
config={employeeConfig}
|
||||
initialData={employees}
|
||||
initialTotalCount={employees.length}
|
||||
externalIsLoading={isLoading}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -553,6 +553,7 @@ export function SalaryManagement() {
|
||||
getItemId: (item) => item.id,
|
||||
}}
|
||||
onSearchChange={setSearchQuery}
|
||||
externalIsLoading={isLoading}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -837,6 +837,7 @@ export function VacationManagement() {
|
||||
onTabChange={handleMainTabChange}
|
||||
onSearchChange={setSearchQuery}
|
||||
onFilterChange={handleFilterChange}
|
||||
externalIsLoading={isLoading}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -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 />;
|
||||
}
|
||||
|
||||
// 에러 상태
|
||||
|
||||
@@ -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} />;
|
||||
}
|
||||
|
||||
// 에러 상태
|
||||
|
||||
@@ -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} />;
|
||||
}
|
||||
|
||||
// 에러 상태
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
|
||||
{/* 개별 삭제 확인 다이얼로그 */}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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">
|
||||
검사 대기 중인 입고 건이 없습니다.
|
||||
|
||||
@@ -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 모드에서)
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -513,6 +513,7 @@ export function WorkOrderCreate() {
|
||||
<IntegratedDetailTemplate
|
||||
config={workOrderCreateConfig}
|
||||
mode="create"
|
||||
isLoading={isLoadingProcesses}
|
||||
isSubmitting={isSubmitting}
|
||||
onBack={handleCancel}
|
||||
onCancel={handleCancel}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
등록된 공정 단계가 없습니다.
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -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) => (
|
||||
|
||||
@@ -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) => (
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
|
||||
{/* 삭제 확인 다이얼로그 */}
|
||||
|
||||
@@ -377,6 +377,13 @@ export interface UniversalListPageProps<T> {
|
||||
* - 외부에서 API 호출 후 데이터 갱신 가능
|
||||
*/
|
||||
onFilterChange?: (filters: Record<string, string | string[]>) => void;
|
||||
/**
|
||||
* 외부 로딩 상태 (서버 사이드 데이터 페칭용)
|
||||
* - 설정 시 내부 isLoading 대신 사용
|
||||
* - 부모 컴포넌트에서 데이터 로딩 상태를 직접 관리할 때 사용
|
||||
* - IntegratedListTemplateV2로 전달되어 스켈레톤 표시에 사용
|
||||
*/
|
||||
externalIsLoading?: boolean;
|
||||
}
|
||||
|
||||
// ===== 내부 상태 타입 =====
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user