From 59bd6541071dfa16425b82828b7bdc9e99487c06 Mon Sep 17 00:00:00 2001 From: dohyeons Date: Wed, 15 Oct 2025 17:11:26 +0900 Subject: [PATCH] =?UTF-8?q?=EB=8C=80=EC=8B=9C=EB=B3=B4=EB=93=9C=20?= =?UTF-8?q?=EA=B4=80=EB=A6=AC=20=EB=AA=A9=EB=A1=9D/=20=ED=8E=B8=EC=A7=91/?= =?UTF-8?q?=20=EB=B7=B0=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../(main)/admin/dashboard/edit/[id]/page.tsx | 23 ++ .../app/(main)/admin/dashboard/new/page.tsx | 15 ++ frontend/app/(main)/admin/dashboard/page.tsx | 236 +++++++++++++++++- .../admin/dashboard/DashboardDesigner.tsx | 19 +- .../admin/dashboard/charts/ChartRenderer.tsx | 9 - .../admin/dashboard/utils/queryHelpers.ts | 12 - .../admin/dashboard/widgets/ListWidget.tsx | 31 ++- 7 files changed, 296 insertions(+), 49 deletions(-) create mode 100644 frontend/app/(main)/admin/dashboard/edit/[id]/page.tsx create mode 100644 frontend/app/(main)/admin/dashboard/new/page.tsx diff --git a/frontend/app/(main)/admin/dashboard/edit/[id]/page.tsx b/frontend/app/(main)/admin/dashboard/edit/[id]/page.tsx new file mode 100644 index 00000000..92220b6c --- /dev/null +++ b/frontend/app/(main)/admin/dashboard/edit/[id]/page.tsx @@ -0,0 +1,23 @@ +"use client"; + +import React from "react"; +import { use } from "react"; +import DashboardDesigner from "@/components/admin/dashboard/DashboardDesigner"; + +interface PageProps { + params: Promise<{ id: string }>; +} + +/** + * 대시보드 편집 페이지 + * - 기존 대시보드 편집 + */ +export default function DashboardEditPage({ params }: PageProps) { + const { id } = use(params); + + return ( +
+ +
+ ); +} diff --git a/frontend/app/(main)/admin/dashboard/new/page.tsx b/frontend/app/(main)/admin/dashboard/new/page.tsx new file mode 100644 index 00000000..d2f2ce11 --- /dev/null +++ b/frontend/app/(main)/admin/dashboard/new/page.tsx @@ -0,0 +1,15 @@ +"use client"; + +import React from "react"; +import DashboardDesigner from "@/components/admin/dashboard/DashboardDesigner"; + +/** + * 새 대시보드 생성 페이지 + */ +export default function DashboardNewPage() { + return ( +
+ +
+ ); +} diff --git a/frontend/app/(main)/admin/dashboard/page.tsx b/frontend/app/(main)/admin/dashboard/page.tsx index cd65cd8a..dcf81963 100644 --- a/frontend/app/(main)/admin/dashboard/page.tsx +++ b/frontend/app/(main)/admin/dashboard/page.tsx @@ -1,18 +1,236 @@ -'use client'; +"use client"; -import React from 'react'; -import DashboardDesigner from '@/components/admin/dashboard/DashboardDesigner'; +import React, { useState, useEffect } from "react"; +import { useRouter } from "next/navigation"; +import { dashboardApi } from "@/lib/api/dashboard"; +import { Dashboard } from "@/lib/api/dashboard"; +import { Button } from "@/components/ui/button"; +import { Card } from "@/components/ui/card"; +import { Input } from "@/components/ui/input"; +import { Badge } from "@/components/ui/badge"; +import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; +import { Plus, Search, MoreVertical, Edit, Trash2, Copy, Eye } from "lucide-react"; /** * 대시보드 관리 페이지 - * - 드래그 앤 드롭으로 대시보드 레이아웃 설계 - * - 차트 및 위젯 배치 관리 - * - 독립적인 컴포넌트로 구성되어 다른 시스템에 영향 없음 + * - 대시보드 목록 조회 + * - 대시보드 생성/수정/삭제/복사 */ -export default function DashboardPage() { +export default function DashboardListPage() { + const router = useRouter(); + const [dashboards, setDashboards] = useState([]); + const [loading, setLoading] = useState(true); + const [searchTerm, setSearchTerm] = useState(""); + const [error, setError] = useState(null); + + // 대시보드 목록 로드 + const loadDashboards = async () => { + try { + setLoading(true); + setError(null); + const result = await dashboardApi.getMyDashboards({ search: searchTerm }); + setDashboards(result.dashboards); + } catch (err) { + console.error("Failed to load dashboards:", err); + setError("대시보드 목록을 불러오는데 실패했습니다."); + } finally { + setLoading(false); + } + }; + + useEffect(() => { + loadDashboards(); + }, [searchTerm]); + + // 대시보드 삭제 + const handleDelete = async (id: string, title: string) => { + if (!confirm(`"${title}" 대시보드를 삭제하시겠습니까?`)) { + return; + } + + try { + await dashboardApi.deleteDashboard(id); + alert("대시보드가 삭제되었습니다."); + loadDashboards(); + } catch (err) { + console.error("Failed to delete dashboard:", err); + alert("대시보드 삭제에 실패했습니다."); + } + }; + + // 대시보드 복사 + const handleCopy = async (dashboard: Dashboard) => { + try { + const newDashboard = await dashboardApi.createDashboard({ + title: `${dashboard.title} (복사본)`, + description: dashboard.description, + elements: dashboard.elements || [], + isPublic: false, + tags: dashboard.tags, + category: dashboard.category, + }); + alert("대시보드가 복사되었습니다."); + loadDashboards(); + } catch (err) { + console.error("Failed to copy dashboard:", err); + alert("대시보드 복사에 실패했습니다."); + } + }; + + // 포맷팅 헬퍼 + const formatDate = (dateString: string) => { + return new Date(dateString).toLocaleDateString("ko-KR", { + year: "numeric", + month: "2-digit", + day: "2-digit", + hour: "2-digit", + minute: "2-digit", + }); + }; + + if (loading) { + return ( +
+
+
로딩 중...
+
대시보드 목록을 불러오고 있습니다
+
+
+ ); + } + return ( -
- +
+
+ {/* 헤더 */} +
+

대시보드 관리

+

대시보드를 생성하고 관리할 수 있습니다

+
+ + {/* 액션 바 */} +
+
+ + setSearchTerm(e.target.value)} + className="pl-9" + /> +
+ +
+ + {/* 에러 메시지 */} + {error && ( + +

{error}

+
+ )} + + {/* 대시보드 목록 */} + {dashboards.length === 0 ? ( + +
+ +
+

대시보드가 없습니다

+

첫 번째 대시보드를 생성하여 데이터 시각화를 시작하세요

+ +
+ ) : ( + + + + + 제목 + 설명 + 요소 수 + 상태 + 생성일 + 수정일 + 작업 + + + + {dashboards.map((dashboard) => ( + + +
+ {dashboard.title} + {dashboard.isPublic && ( + + 공개 + + )} +
+
+ + {dashboard.description || "-"} + + + {dashboard.elementsCount || 0}개 + + + {dashboard.isPublic ? ( + 공개 + ) : ( + 비공개 + )} + + {formatDate(dashboard.createdAt)} + {formatDate(dashboard.updatedAt)} + + + + + + + router.push(`/dashboard/${dashboard.id}`)} className="gap-2"> + + 보기 + + router.push(`/admin/dashboard/edit/${dashboard.id}`)} + className="gap-2" + > + + 편집 + + handleCopy(dashboard)} className="gap-2"> + + 복사 + + handleDelete(dashboard.id, dashboard.title)} + className="gap-2 text-red-600 focus:text-red-600" + > + + 삭제 + + + + +
+ ))} +
+
+
+ )} +
); } diff --git a/frontend/components/admin/dashboard/DashboardDesigner.tsx b/frontend/components/admin/dashboard/DashboardDesigner.tsx index abb328b0..65ba514c 100644 --- a/frontend/components/admin/dashboard/DashboardDesigner.tsx +++ b/frontend/components/admin/dashboard/DashboardDesigner.tsx @@ -10,6 +10,10 @@ import { ListWidgetConfigModal } from "./widgets/ListWidgetConfigModal"; import { DashboardElement, ElementType, ElementSubtype } from "./types"; import { GRID_CONFIG } from "./gridUtils"; +interface DashboardDesignerProps { + dashboardId?: string; +} + /** * 대시보드 설계 도구 메인 컴포넌트 * - 드래그 앤 드롭으로 차트/위젯 배치 @@ -17,27 +21,24 @@ import { GRID_CONFIG } from "./gridUtils"; * - 요소 이동, 크기 조절, 삭제 기능 * - 레이아웃 저장/불러오기 기능 */ -export default function DashboardDesigner() { +export default function DashboardDesigner({ dashboardId: initialDashboardId }: DashboardDesignerProps = {}) { const router = useRouter(); const [elements, setElements] = useState([]); const [selectedElement, setSelectedElement] = useState(null); const [elementCounter, setElementCounter] = useState(0); const [configModalElement, setConfigModalElement] = useState(null); - const [dashboardId, setDashboardId] = useState(null); + const [dashboardId, setDashboardId] = useState(initialDashboardId || null); const [dashboardTitle, setDashboardTitle] = useState(""); const [isLoading, setIsLoading] = useState(false); const [canvasBackgroundColor, setCanvasBackgroundColor] = useState("#f9fafb"); const canvasRef = useRef(null); - // URL 파라미터에서 대시보드 ID 읽기 및 데이터 로드 + // 대시보드 ID가 props로 전달되면 로드 React.useEffect(() => { - const params = new URLSearchParams(window.location.search); - const loadId = params.get("load"); - - if (loadId) { - loadDashboard(loadId); + if (initialDashboardId) { + loadDashboard(initialDashboardId); } - }, []); + }, [initialDashboardId]); // 대시보드 데이터 로드 const loadDashboard = async (id: string) => { diff --git a/frontend/components/admin/dashboard/charts/ChartRenderer.tsx b/frontend/components/admin/dashboard/charts/ChartRenderer.tsx index ce703662..092c2b0a 100644 --- a/frontend/components/admin/dashboard/charts/ChartRenderer.tsx +++ b/frontend/components/admin/dashboard/charts/ChartRenderer.tsx @@ -125,14 +125,7 @@ export function ChartRenderer({ element, data, width = 250, height = 200 }: Char }; } else { // 현재 DB - 필터 적용 - console.log("📊 ChartRenderer: 현재 DB 쿼리 실행"); - console.log(" 원본 쿼리:", element.dataSource.query); - console.log(" chartConfig:", element.chartConfig); - const filteredQuery = applyQueryFilters(element.dataSource.query, element.chartConfig); - - console.log(" 필터 적용된 쿼리:", filteredQuery); - const result = await dashboardApi.executeQuery(filteredQuery); queryResult = { columns: result.columns, @@ -140,8 +133,6 @@ export function ChartRenderer({ element, data, width = 250, height = 200 }: Char totalRows: result.rowCount, executionTime: 0, }; - - console.log(" 쿼리 결과:", queryResult); } } else { throw new Error("데이터 소스가 올바르게 설정되지 않았습니다"); diff --git a/frontend/components/admin/dashboard/utils/queryHelpers.ts b/frontend/components/admin/dashboard/utils/queryHelpers.ts index 9fc9fbbb..b5220eb4 100644 --- a/frontend/components/admin/dashboard/utils/queryHelpers.ts +++ b/frontend/components/admin/dashboard/utils/queryHelpers.ts @@ -88,11 +88,6 @@ export function applyDateFilter(query: string, dateColumn: string, startDate?: s // 쿼리 재조립 const finalQuery = `${baseQuery}${whereClause}${groupByClause}${orderByClause}${limitClause}`; - - console.log("🔧 날짜 필터 적용:"); - console.log(" 원본 쿼리:", query); - console.log(" 최종 쿼리:", finalQuery); - return finalQuery; } @@ -233,23 +228,16 @@ export function detectDateColumns(columns: string[], rows: Record[] * 쿼리에 필터와 안전장치를 모두 적용 */ export function applyQueryFilters(query: string, config?: ChartConfig): string { - console.log("🔍 applyQueryFilters 호출:"); - console.log(" config:", config); - console.log(" dateFilter:", config?.dateFilter); - let processedQuery = query; // 1. 날짜 필터 적용 if (config?.dateFilter?.enabled && config.dateFilter.dateColumn) { - console.log("✅ 날짜 필터 적용 중..."); processedQuery = applyDateFilter( processedQuery, config.dateFilter.dateColumn, config.dateFilter.startDate, config.dateFilter.endDate, ); - } else { - console.log("⚠️ 날짜 필터 비활성화 또는 설정 없음"); } // 2. 안전장치 LIMIT 적용 diff --git a/frontend/components/admin/dashboard/widgets/ListWidget.tsx b/frontend/components/admin/dashboard/widgets/ListWidget.tsx index 390767f4..ec432299 100644 --- a/frontend/components/admin/dashboard/widgets/ListWidget.tsx +++ b/frontend/components/admin/dashboard/widgets/ListWidget.tsx @@ -187,8 +187,8 @@ export function ListWidget({ element, onConfigUpdate }: ListWidgetProps) { ); } - // 데이터 또는 설정 없음 - if (!data || config.columns.length === 0) { + // 데이터 없음 + if (!data) { return (
@@ -200,6 +200,17 @@ export function ListWidget({ element, onConfigUpdate }: ListWidgetProps) { ); } + // 컬럼 설정이 없으면 자동으로 모든 컬럼 표시 + const displayColumns = + config.columns.length > 0 + ? config.columns + : data.columns.map((col) => ({ + id: col, + name: col, + dataKey: col, + visible: true, + })); + // 페이지네이션 const totalPages = Math.ceil(data.rows.length / config.pageSize); const startIdx = (currentPage - 1) * config.pageSize; @@ -219,7 +230,7 @@ export function ListWidget({ element, onConfigUpdate }: ListWidgetProps) { {config.showHeader && ( - {config.columns + {displayColumns .filter((col) => col.visible) .map((col) => ( - {col.label} + {col.label || col.name} ))} @@ -237,7 +248,7 @@ export function ListWidget({ element, onConfigUpdate }: ListWidgetProps) { {paginatedRows.length === 0 ? ( col.visible).length} + colSpan={displayColumns.filter((col) => col.visible).length} className="text-center text-gray-500" > 데이터가 없습니다 @@ -246,14 +257,14 @@ export function ListWidget({ element, onConfigUpdate }: ListWidgetProps) { ) : ( paginatedRows.map((row, idx) => ( - {config.columns + {displayColumns .filter((col) => col.visible) .map((col) => ( - {String(row[col.field] ?? "")} + {String(row[col.dataKey || col.field] ?? "")} ))} @@ -279,15 +290,15 @@ export function ListWidget({ element, onConfigUpdate }: ListWidgetProps) { {paginatedRows.map((row, idx) => (
- {config.columns + {displayColumns .filter((col) => col.visible) .map((col) => (
-
{col.label}
+
{col.label || col.name}
- {String(row[col.field] ?? "")} + {String(row[col.dataKey || col.field] ?? "")}
))}