From eebf80e0286d55401f692786a1787ede9431dad8 Mon Sep 17 00:00:00 2001 From: dohyeons Date: Thu, 23 Oct 2025 17:23:30 +0900 Subject: [PATCH 1/2] =?UTF-8?q?=EC=B0=A8=ED=8A=B8=20=EA=B0=84=20=EA=B3=B5?= =?UTF-8?q?=EC=9C=A0=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../admin/dashboard/CanvasElement.tsx | 68 +++++++++++++++++-- .../admin/dashboard/ChartConfigPanel.tsx | 6 +- .../admin/dashboard/DashboardTopMenu.tsx | 7 +- frontend/components/admin/dashboard/types.ts | 34 ++++++++++ 4 files changed, 105 insertions(+), 10 deletions(-) diff --git a/frontend/components/admin/dashboard/CanvasElement.tsx b/frontend/components/admin/dashboard/CanvasElement.tsx index 530e510e..33b1d801 100644 --- a/frontend/components/admin/dashboard/CanvasElement.tsx +++ b/frontend/components/admin/dashboard/CanvasElement.tsx @@ -2,9 +2,26 @@ import React, { useState, useCallback, useRef, useEffect } from "react"; import dynamic from "next/dynamic"; -import { DashboardElement, QueryResult, Position } from "./types"; +import { + DashboardElement, + QueryResult, + Position, + ElementSubtype, + AXIS_BASED_CHARTS, + CIRCULAR_CHARTS, + getChartCategory, +} from "./types"; import { ChartRenderer } from "./charts/ChartRenderer"; import { GRID_CONFIG, magneticSnap, snapSizeToGrid } from "./gridUtils"; +import { + Select, + SelectContent, + SelectGroup, + SelectItem, + SelectLabel, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; // 위젯 동적 임포트 const WeatherWidget = dynamic(() => import("@/components/dashboard/widgets/WeatherWidget"), { @@ -710,14 +727,54 @@ export function CanvasElement({ top: displayPosition.y, width: displaySize.width, height: displaySize.height, - padding: `${GRID_CONFIG.ELEMENT_PADDING}px`, boxSizing: "border-box", }} onMouseDown={handleMouseDown} > {/* 헤더 */} -
- {element.customTitle || element.title} +
+
+ {/* 차트 타입 전환 드롭다운 (차트일 경우만) */} + {element.type === "chart" && ( + + )} + {/* 제목 */} + {!element.type || element.type !== "chart" ? ( + {element.customTitle || element.title} + ) : null} +
{/* 삭제 버튼 */}
{/* 내용 */} -
+
{element.type === "chart" ? ( // 차트 렌더링
diff --git a/frontend/components/admin/dashboard/ChartConfigPanel.tsx b/frontend/components/admin/dashboard/ChartConfigPanel.tsx index 04dd2d0e..703530c2 100644 --- a/frontend/components/admin/dashboard/ChartConfigPanel.tsx +++ b/frontend/components/admin/dashboard/ChartConfigPanel.tsx @@ -1,7 +1,7 @@ "use client"; import React, { useState, useCallback, useEffect } from "react"; -import { ChartConfig, QueryResult } from "./types"; +import { ChartConfig, QueryResult, isCircularChart, isAxisBasedChart } from "./types"; import { Input } from "@/components/ui/input"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { Label } from "@/components/ui/label"; @@ -40,8 +40,8 @@ export function ChartConfigPanel({ const [currentConfig, setCurrentConfig] = useState(config || {}); const [dateColumns, setDateColumns] = useState([]); - // 원형/도넛 차트 또는 REST API는 Y축이 필수가 아님 - const isPieChart = chartType === "pie" || chartType === "donut"; + // 원형 차트 또는 REST API는 Y축이 필수가 아님 + const isPieChart = chartType ? isCircularChart(chartType as any) : false; const isApiSource = dataSourceType === "api"; // 설정 업데이트 diff --git a/frontend/components/admin/dashboard/DashboardTopMenu.tsx b/frontend/components/admin/dashboard/DashboardTopMenu.tsx index 6392b355..b9e5976d 100644 --- a/frontend/components/admin/dashboard/DashboardTopMenu.tsx +++ b/frontend/components/admin/dashboard/DashboardTopMenu.tsx @@ -159,15 +159,18 @@ export function DashboardTopMenu({ - 차트 + 축 기반 차트 바 차트 수평 바 차트 누적 바 차트 꺾은선 차트 영역 차트 + 콤보 차트 + + + 원형 차트 원형 차트 도넛 차트 - 콤보 차트 diff --git a/frontend/components/admin/dashboard/types.ts b/frontend/components/admin/dashboard/types.ts index a07b5247..71a86a13 100644 --- a/frontend/components/admin/dashboard/types.ts +++ b/frontend/components/admin/dashboard/types.ts @@ -42,6 +42,40 @@ export type ElementSubtype = | "transport-stats" // 커스텀 통계 카드 위젯 | "custom-metric"; // 사용자 커스텀 카드 위젯 +// 차트 분류 +export type ChartCategory = "axis-based" | "circular"; + +// 축 기반 차트 (X/Y축 사용) +export const AXIS_BASED_CHARTS = ["bar", "horizontal-bar", "line", "area", "stacked-bar", "combo"] as const; + +// 원형 차트 (그룹핑 사용) +export const CIRCULAR_CHARTS = ["pie", "donut"] as const; + +// 차트인지 확인 +export const isChartType = (subtype: ElementSubtype): boolean => { + return ( + (AXIS_BASED_CHARTS as readonly string[]).includes(subtype) || + (CIRCULAR_CHARTS as readonly string[]).includes(subtype) + ); +}; + +// 축 기반 차트인지 확인 +export const isAxisBasedChart = (subtype: ElementSubtype): boolean => { + return (AXIS_BASED_CHARTS as readonly string[]).includes(subtype); +}; + +// 원형 차트인지 확인 +export const isCircularChart = (subtype: ElementSubtype): boolean => { + return (CIRCULAR_CHARTS as readonly string[]).includes(subtype); +}; + +// 차트 카테고리 가져오기 +export const getChartCategory = (subtype: ElementSubtype): ChartCategory | null => { + if (isAxisBasedChart(subtype)) return "axis-based"; + if (isCircularChart(subtype)) return "circular"; + return null; +}; + export interface Position { x: number; y: number; From 67e838dc03494e528e78d573bb38200cfc4e8d0d Mon Sep 17 00:00:00 2001 From: dohyeons Date: Thu, 23 Oct 2025 17:44:12 +0900 Subject: [PATCH 2/2] =?UTF-8?q?package-lock=20=ED=8C=8C=EC=9D=BC=20?= =?UTF-8?q?=EC=9E=AC=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend-node/package-lock.json | 68 +++++++++++++++++++++++++--------- 1 file changed, 51 insertions(+), 17 deletions(-) diff --git a/backend-node/package-lock.json b/backend-node/package-lock.json index 81adfc5c..b9528ee0 100644 --- a/backend-node/package-lock.json +++ b/backend-node/package-lock.json @@ -7605,7 +7605,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "dev": true, "license": "MIT" }, "node_modules/js-yaml": { @@ -7949,6 +7948,19 @@ "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", "license": "Apache-2.0" }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "license": "MIT", + "peer": true, + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, "node_modules/lru-cache": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", @@ -7992,22 +8004,6 @@ "tlds": "1.260.0" } }, - "node_modules/mailparser/node_modules/iconv-lite": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.0.tgz", - "integrity": "sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ==", - "license": "MIT", - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3.0.0" - }, - "engines": { - "node": ">=0.10.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, "node_modules/mailparser/node_modules/nodemailer": { "version": "7.0.9", "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-7.0.9.tgz", @@ -8021,6 +8017,7 @@ "version": "5.4.6", "resolved": "https://registry.npmjs.org/mailsplit/-/mailsplit-5.4.6.tgz", "integrity": "sha512-M+cqmzaPG/mEiCDmqQUz8L177JZLZmXAUpq38owtpq2xlXlTSw+kntnxRt2xsxVFFV6+T8Mj/U0l5s7s6e0rNw==", + "deprecated": "This package has been renamed to @zone-eu/mailsplit. Please update your dependencies.", "license": "(MIT OR EUPL-1.1+)", "dependencies": { "libbase64": "1.3.0", @@ -9268,6 +9265,33 @@ "node": ">=0.10.0" } }, + "node_modules/react": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "license": "MIT", + "peer": true, + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.2" + }, + "peerDependencies": { + "react": "^18.3.1" + } + }, "node_modules/react-is": { "version": "18.3.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", @@ -9586,6 +9610,16 @@ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", "license": "MIT" }, + "node_modules/scheduler": { + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "loose-envify": "^1.1.0" + } + }, "node_modules/selderee": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/selderee/-/selderee-0.11.0.tgz",