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

276 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="bg-background min-h-screen">
{/* 헤더 */}
<div className="border-border bg-card border-b">
<div className="mx-auto max-w-7xl px-6 py-6">
<div className="flex items-center justify-between">
<div>
<h1 className="text-foreground text-3xl font-bold">📊 </h1>
<p className="text-muted-foreground mt-1"> </p>
</div>
<Link
href="/admin/screenMng/dashboardList"
className="bg-primary text-primary-foreground hover:bg-primary/90 rounded-lg px-6 py-3 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="border-input bg-background text-foreground focus-visible:ring-ring w-full rounded-lg border py-2 pr-4 pl-10 focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none"
/>
<div className="text-muted-foreground absolute top-2.5 left-3">🔍</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="border-border bg-card rounded-lg border p-6 shadow-sm">
<div className="animate-pulse">
<div className="bg-muted mb-3 h-4 w-3/4 rounded"></div>
<div className="bg-muted mb-2 h-3 w-full rounded"></div>
<div className="bg-muted mb-4 h-3 w-2/3 rounded"></div>
<div className="bg-muted mb-4 h-32 rounded"></div>
<div className="flex justify-between">
<div className="bg-muted h-3 w-1/4 rounded"></div>
<div className="bg-muted h-3 w-1/4 rounded"></div>
</div>
</div>
</div>
))}
</div>
) : filteredDashboards.length === 0 ? (
// 빈 상태
<div className="py-12 text-center">
<div className="mb-4 text-6xl">📊</div>
<h3 className="text-foreground mb-2 text-xl font-medium">
{searchTerm ? "검색 결과가 없습니다" : "아직 대시보드가 없습니다"}
</h3>
<p className="text-muted-foreground mb-6">
{searchTerm ? "다른 검색어로 시도해보세요" : "첫 번째 대시보드를 만들어보세요"}
</p>
{!searchTerm && (
<Link
href="/admin/screenMng/dashboardList"
className="bg-primary text-primary-foreground hover:bg-primary/90 inline-flex items-center rounded-lg px-6 py-3 font-medium"
>
</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="border-border bg-card rounded-lg border shadow-sm transition-shadow hover:shadow-md">
{/* 썸네일 영역 */}
<div className="from-primary/10 to-primary/20 flex h-48 items-center justify-center rounded-t-lg bg-gradient-to-br">
<div className="text-center">
<div className="mb-2 text-4xl">📊</div>
<div className="text-muted-foreground text-sm">{dashboard.elementsCount} </div>
</div>
</div>
{/* 카드 내용 */}
<div className="p-6">
<div className="mb-3 flex items-start justify-between">
<h3 className="text-foreground line-clamp-1 text-lg font-semibold">{dashboard.title}</h3>
{dashboard.isPublic ? (
<span className="bg-success/10 text-success rounded-full px-2 py-1 text-xs"></span>
) : (
<span className="bg-muted text-muted-foreground rounded-full px-2 py-1 text-xs"></span>
)}
</div>
{dashboard.description && (
<p className="text-muted-foreground mb-4 line-clamp-2 text-sm">{dashboard.description}</p>
)}
{/* 메타 정보 */}
<div className="text-muted-foreground mb-4 text-xs">
<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="bg-primary text-primary-foreground hover:bg-primary/90 flex-1 rounded-lg px-4 py-2 text-center text-sm font-medium"
>
</Link>
<Link
href={`/admin/screenMng/dashboardList?load=${dashboard.id}`}
className="border-input bg-background text-foreground hover:bg-accent hover:text-accent-foreground rounded-lg border px-4 py-2 text-sm"
>
</Link>
<button
onClick={() => {
// 복사 기능 구현
console.log("Dashboard copy:", dashboard.id);
}}
className="border-input bg-background text-foreground hover:bg-accent hover:text-accent-foreground rounded-lg border px-4 py-2 text-sm"
title="복사"
>
📋
</button>
</div>
</div>
</div>
);
}