ERP-node/frontend/app/(main)/dashboard/page.tsx

287 lines
10 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

'use client';
import React, { useState, useEffect } from 'react';
import Link from 'next/link';
interface Dashboard {
id: string;
title: string;
description?: string;
thumbnail?: string;
elementsCount: number;
createdAt: string;
updatedAt: string;
isPublic: boolean;
}
/**
* 대시보드 목록 페이지
* - 저장된 대시보드들의 목록 표시
* - 새 대시보드 생성 링크
* - 대시보드 미리보기 및 관리
*/
export default function DashboardListPage() {
const [dashboards, setDashboards] = useState<Dashboard[]>([]);
const [isLoading, setIsLoading] = useState(true);
const [searchTerm, setSearchTerm] = useState('');
// 대시보드 목록 로딩
useEffect(() => {
loadDashboards();
}, []);
const loadDashboards = async () => {
setIsLoading(true);
try {
// 실제 API 호출 시도
const { dashboardApi } = await import('@/lib/api/dashboard');
try {
const result = await dashboardApi.getDashboards({ page: 1, limit: 50 });
// API에서 가져온 대시보드들을 Dashboard 형식으로 변환
const apiDashboards: Dashboard[] = result.dashboards.map((dashboard: any) => ({
id: dashboard.id,
title: dashboard.title,
description: dashboard.description,
elementsCount: dashboard.elementsCount || dashboard.elements?.length || 0,
createdAt: dashboard.createdAt,
updatedAt: dashboard.updatedAt,
isPublic: dashboard.isPublic,
creatorName: dashboard.creatorName
}));
setDashboards(apiDashboards);
} catch (apiError) {
console.warn('API 호출 실패, 로컬 스토리지 및 샘플 데이터 사용:', apiError);
// API 실패 시 로컬 스토리지 + 샘플 데이터 사용
const savedDashboards = JSON.parse(localStorage.getItem('savedDashboards') || '[]');
// 샘플 대시보드들
const sampleDashboards: Dashboard[] = [
{
id: 'sales-overview',
title: '📊 매출 현황 대시보드',
description: '월별 매출 추이 및 상품별 판매 현황을 한눈에 확인할 수 있습니다.',
elementsCount: 3,
createdAt: '2024-09-30T10:00:00Z',
updatedAt: '2024-09-30T14:30:00Z',
isPublic: true
},
{
id: 'user-analytics',
title: '👥 사용자 분석 대시보드',
description: '사용자 행동 패턴 및 가입 추이 분석',
elementsCount: 1,
createdAt: '2024-09-29T15:00:00Z',
updatedAt: '2024-09-30T09:15:00Z',
isPublic: false
},
{
id: 'inventory-status',
title: '📦 재고 현황 대시보드',
description: '실시간 재고 현황 및 입출고 내역',
elementsCount: 4,
createdAt: '2024-09-28T11:30:00Z',
updatedAt: '2024-09-29T16:45:00Z',
isPublic: true
}
];
// 저장된 대시보드를 Dashboard 형식으로 변환
const userDashboards: Dashboard[] = savedDashboards.map((dashboard: any) => ({
id: dashboard.id,
title: dashboard.title,
description: dashboard.description,
elementsCount: dashboard.elements?.length || 0,
createdAt: dashboard.createdAt,
updatedAt: dashboard.updatedAt,
isPublic: false // 사용자가 만든 대시보드는 기본적으로 비공개
}));
// 사용자 대시보드를 맨 앞에 배치
setDashboards([...userDashboards, ...sampleDashboards]);
}
} catch (error) {
console.error('Dashboard loading error:', error);
} finally {
setIsLoading(false);
}
};
// 검색 필터링
const filteredDashboards = dashboards.filter(dashboard =>
dashboard.title.toLowerCase().includes(searchTerm.toLowerCase()) ||
dashboard.description?.toLowerCase().includes(searchTerm.toLowerCase())
);
return (
<div className="min-h-screen bg-gray-50">
{/* 헤더 */}
<div className="bg-white border-b border-gray-200">
<div className="max-w-7xl mx-auto px-6 py-6">
<div className="flex justify-between items-center">
<div>
<h1 className="text-3xl font-bold text-gray-900">📊 </h1>
<p className="text-gray-600 mt-1"> </p>
</div>
<Link
href="/admin/dashboard"
className="px-6 py-3 bg-blue-500 text-white rounded-lg hover:bg-blue-600 font-medium"
>
</Link>
</div>
{/* 검색 바 */}
<div className="mt-6">
<div className="relative max-w-md">
<input
type="text"
placeholder="대시보드 검색..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="w-full pl-10 pr-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
/>
<div className="absolute left-3 top-2.5 text-gray-400">
🔍
</div>
</div>
</div>
</div>
</div>
{/* 메인 콘텐츠 */}
<div className="max-w-7xl mx-auto px-6 py-8">
{isLoading ? (
// 로딩 상태
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{[1, 2, 3, 4, 5, 6].map((i) => (
<div key={i} className="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
<div className="animate-pulse">
<div className="h-4 bg-gray-200 rounded w-3/4 mb-3"></div>
<div className="h-3 bg-gray-200 rounded w-full mb-2"></div>
<div className="h-3 bg-gray-200 rounded w-2/3 mb-4"></div>
<div className="h-32 bg-gray-200 rounded mb-4"></div>
<div className="flex justify-between">
<div className="h-3 bg-gray-200 rounded w-1/4"></div>
<div className="h-3 bg-gray-200 rounded w-1/4"></div>
</div>
</div>
</div>
))}
</div>
) : filteredDashboards.length === 0 ? (
// 빈 상태
<div className="text-center py-12">
<div className="text-6xl mb-4">📊</div>
<h3 className="text-xl font-medium text-gray-700 mb-2">
{searchTerm ? '검색 결과가 없습니다' : '아직 대시보드가 없습니다'}
</h3>
<p className="text-gray-500 mb-6">
{searchTerm
? '다른 검색어로 시도해보세요'
: '첫 번째 대시보드를 만들어보세요'}
</p>
{!searchTerm && (
<Link
href="/admin/dashboard"
className="inline-flex items-center px-6 py-3 bg-blue-500 text-white rounded-lg hover:bg-blue-600 font-medium"
>
</Link>
)}
</div>
) : (
// 대시보드 그리드
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{filteredDashboards.map((dashboard) => (
<DashboardCard key={dashboard.id} dashboard={dashboard} />
))}
</div>
)}
</div>
</div>
);
}
interface DashboardCardProps {
dashboard: Dashboard;
}
/**
* 개별 대시보드 카드 컴포넌트
*/
function DashboardCard({ dashboard }: DashboardCardProps) {
return (
<div className="bg-white rounded-lg shadow-sm border border-gray-200 hover:shadow-md transition-shadow">
{/* 썸네일 영역 */}
<div className="h-48 bg-gradient-to-br from-blue-50 to-indigo-100 rounded-t-lg flex items-center justify-center">
<div className="text-center">
<div className="text-4xl mb-2">📊</div>
<div className="text-sm text-gray-600">{dashboard.elementsCount} </div>
</div>
</div>
{/* 카드 내용 */}
<div className="p-6">
<div className="flex justify-between items-start mb-3">
<h3 className="text-lg font-semibold text-gray-900 line-clamp-1">
{dashboard.title}
</h3>
{dashboard.isPublic ? (
<span className="text-xs bg-green-100 text-green-800 px-2 py-1 rounded-full">
</span>
) : (
<span className="text-xs bg-gray-100 text-gray-800 px-2 py-1 rounded-full">
</span>
)}
</div>
{dashboard.description && (
<p className="text-gray-600 text-sm mb-4 line-clamp-2">
{dashboard.description}
</p>
)}
{/* 메타 정보 */}
<div className="text-xs text-gray-500 mb-4">
<div>: {new Date(dashboard.createdAt).toLocaleDateString()}</div>
<div>: {new Date(dashboard.updatedAt).toLocaleDateString()}</div>
</div>
{/* 액션 버튼들 */}
<div className="flex gap-2">
<Link
href={`/dashboard/${dashboard.id}`}
className="flex-1 px-4 py-2 bg-blue-500 text-white text-center rounded-lg hover:bg-blue-600 text-sm font-medium"
>
</Link>
<Link
href={`/admin/dashboard?load=${dashboard.id}`}
className="px-4 py-2 border border-gray-300 text-gray-700 rounded-lg hover:bg-gray-50 text-sm"
>
</Link>
<button
onClick={() => {
// 복사 기능 구현
console.log('Dashboard copy:', dashboard.id);
}}
className="px-4 py-2 border border-gray-300 text-gray-700 rounded-lg hover:bg-gray-50 text-sm"
title="복사"
>
📋
</button>
</div>
</div>
</div>
);
}