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