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

274 lines
9.9 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="border-b border-gray-200 bg-white">
<div className="mx-auto max-w-7xl px-6 py-6">
<div className="flex items-center justify-between">
<div>
<h1 className="text-3xl font-bold text-gray-900">📊 </h1>
<p className="mt-1 text-gray-600"> </p>
</div>
<Link
href="/admin/dashboard"
className="rounded-lg bg-blue-500 px-6 py-3 font-medium text-white hover:bg-blue-600"
>
</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 rounded-lg border border-gray-300 py-2 pr-4 pl-10 focus:border-transparent focus:ring-2 focus:ring-blue-500"
/>
<div className="absolute top-2.5 left-3 text-gray-400">🔍</div>
</div>
</div>
</div>
</div>
{/* 메인 콘텐츠 */}
<div className="mx-auto max-w-7xl px-6 py-8">
{isLoading ? (
// 로딩 상태
<div className="grid grid-cols-1 gap-6 md:grid-cols-2 lg:grid-cols-3">
{[1, 2, 3, 4, 5, 6].map((i) => (
<div key={i} className="rounded-lg border border-gray-200 bg-white p-6 shadow-sm">
<div className="animate-pulse">
<div className="mb-3 h-4 w-3/4 rounded bg-gray-200"></div>
<div className="mb-2 h-3 w-full rounded bg-gray-200"></div>
<div className="mb-4 h-3 w-2/3 rounded bg-gray-200"></div>
<div className="mb-4 h-32 rounded bg-gray-200"></div>
<div className="flex justify-between">
<div className="h-3 w-1/4 rounded bg-gray-200"></div>
<div className="h-3 w-1/4 rounded bg-gray-200"></div>
</div>
</div>
</div>
))}
</div>
) : filteredDashboards.length === 0 ? (
// 빈 상태
<div className="py-12 text-center">
<div className="mb-4 text-6xl">📊</div>
<h3 className="mb-2 text-xl font-medium text-gray-700">
{searchTerm ? "검색 결과가 없습니다" : "아직 대시보드가 없습니다"}
</h3>
<p className="mb-6 text-gray-500">
{searchTerm ? "다른 검색어로 시도해보세요" : "첫 번째 대시보드를 만들어보세요"}
</p>
{!searchTerm && (
<Link
href="/admin/dashboard"
className="inline-flex items-center rounded-lg bg-blue-500 px-6 py-3 font-medium text-white hover:bg-blue-600"
>
</Link>
)}
</div>
) : (
// 대시보드 그리드
<div className="grid grid-cols-1 gap-6 md:grid-cols-2 lg:grid-cols-3">
{filteredDashboards.map((dashboard) => (
<DashboardCard key={dashboard.id} dashboard={dashboard} />
))}
</div>
)}
</div>
</div>
);
}
interface DashboardCardProps {
dashboard: Dashboard;
}
/**
* 개별 대시보드 카드 컴포넌트
*/
function DashboardCard({ dashboard }: DashboardCardProps) {
return (
<div className="rounded-lg border border-gray-200 bg-white shadow-sm transition-shadow hover:shadow-md">
{/* 썸네일 영역 */}
<div className="flex h-48 items-center justify-center rounded-t-lg bg-gradient-to-br from-blue-50 to-indigo-100">
<div className="text-center">
<div className="mb-2 text-4xl">📊</div>
<div className="text-sm text-gray-600">{dashboard.elementsCount} </div>
</div>
</div>
{/* 카드 내용 */}
<div className="p-6">
<div className="mb-3 flex items-start justify-between">
<h3 className="line-clamp-1 text-lg font-semibold text-gray-900">{dashboard.title}</h3>
{dashboard.isPublic ? (
<span className="rounded-full bg-green-100 px-2 py-1 text-xs text-green-800"></span>
) : (
<span className="rounded-full bg-gray-100 px-2 py-1 text-xs text-gray-800"></span>
)}
</div>
{dashboard.description && <p className="mb-4 line-clamp-2 text-sm text-gray-600">{dashboard.description}</p>}
{/* 메타 정보 */}
<div className="mb-4 text-xs text-gray-500">
<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 rounded-lg bg-blue-500 px-4 py-2 text-center text-sm font-medium text-white hover:bg-blue-600"
>
</Link>
<Link
href={`/admin/dashboard?load=${dashboard.id}`}
className="rounded-lg border border-gray-300 px-4 py-2 text-sm text-gray-700 hover:bg-gray-50"
>
</Link>
<button
onClick={() => {
// 복사 기능 구현
console.log("Dashboard copy:", dashboard.id);
}}
className="rounded-lg border border-gray-300 px-4 py-2 text-sm text-gray-700 hover:bg-gray-50"
title="복사"
>
📋
</button>
</div>
</div>
</div>
);
}