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>
);
}