/dashboard 최적화 진행

This commit is contained in:
dohyeons 2025-10-30 10:07:44 +09:00
parent 8f38b176ab
commit 234f82b944
1 changed files with 85 additions and 99 deletions

View File

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