Compare commits
7 Commits
43654f7516
...
2f51b9632d
| Author | SHA1 | Date |
|---|---|---|
|
|
2f51b9632d | |
|
|
67e838dc03 | |
|
|
4ca4ea3b3c | |
|
|
bb926b1c58 | |
|
|
b242a85801 | |
|
|
5fa335a83e | |
|
|
eebf80e028 |
|
|
@ -7605,7 +7605,6 @@
|
||||||
"version": "4.0.0",
|
"version": "4.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
||||||
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
|
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/js-yaml": {
|
"node_modules/js-yaml": {
|
||||||
|
|
@ -7949,6 +7948,19 @@
|
||||||
"integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==",
|
"integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==",
|
||||||
"license": "Apache-2.0"
|
"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": {
|
"node_modules/lru-cache": {
|
||||||
"version": "5.1.1",
|
"version": "5.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
|
||||||
|
|
@ -7992,22 +8004,6 @@
|
||||||
"tlds": "1.260.0"
|
"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": {
|
"node_modules/mailparser/node_modules/nodemailer": {
|
||||||
"version": "7.0.9",
|
"version": "7.0.9",
|
||||||
"resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-7.0.9.tgz",
|
"resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-7.0.9.tgz",
|
||||||
|
|
@ -8021,6 +8017,7 @@
|
||||||
"version": "5.4.6",
|
"version": "5.4.6",
|
||||||
"resolved": "https://registry.npmjs.org/mailsplit/-/mailsplit-5.4.6.tgz",
|
"resolved": "https://registry.npmjs.org/mailsplit/-/mailsplit-5.4.6.tgz",
|
||||||
"integrity": "sha512-M+cqmzaPG/mEiCDmqQUz8L177JZLZmXAUpq38owtpq2xlXlTSw+kntnxRt2xsxVFFV6+T8Mj/U0l5s7s6e0rNw==",
|
"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+)",
|
"license": "(MIT OR EUPL-1.1+)",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"libbase64": "1.3.0",
|
"libbase64": "1.3.0",
|
||||||
|
|
@ -9268,6 +9265,33 @@
|
||||||
"node": ">=0.10.0"
|
"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": {
|
"node_modules/react-is": {
|
||||||
"version": "18.3.1",
|
"version": "18.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz",
|
||||||
|
|
@ -9586,6 +9610,16 @@
|
||||||
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
|
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/selderee": {
|
||||||
"version": "0.11.0",
|
"version": "0.11.0",
|
||||||
"resolved": "https://registry.npmjs.org/selderee/-/selderee-0.11.0.tgz",
|
"resolved": "https://registry.npmjs.org/selderee/-/selderee-0.11.0.tgz",
|
||||||
|
|
|
||||||
|
|
@ -2,9 +2,26 @@
|
||||||
|
|
||||||
import React, { useState, useCallback, useRef, useEffect } from "react";
|
import React, { useState, useCallback, useRef, useEffect } from "react";
|
||||||
import dynamic from "next/dynamic";
|
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 { ChartRenderer } from "./charts/ChartRenderer";
|
||||||
import { GRID_CONFIG, magneticSnap, snapSizeToGrid } from "./gridUtils";
|
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"), {
|
const WeatherWidget = dynamic(() => import("@/components/dashboard/widgets/WeatherWidget"), {
|
||||||
|
|
@ -710,14 +727,54 @@ export function CanvasElement({
|
||||||
top: displayPosition.y,
|
top: displayPosition.y,
|
||||||
width: displaySize.width,
|
width: displaySize.width,
|
||||||
height: displaySize.height,
|
height: displaySize.height,
|
||||||
padding: `${GRID_CONFIG.ELEMENT_PADDING}px`,
|
|
||||||
boxSizing: "border-box",
|
boxSizing: "border-box",
|
||||||
}}
|
}}
|
||||||
onMouseDown={handleMouseDown}
|
onMouseDown={handleMouseDown}
|
||||||
>
|
>
|
||||||
{/* 헤더 */}
|
{/* 헤더 */}
|
||||||
<div className="flex cursor-move items-center justify-between p-3">
|
<div className="flex cursor-move items-center justify-between px-4 py-2">
|
||||||
<span className="text-sm font-bold text-gray-800">{element.customTitle || element.title}</span>
|
<div className="flex items-center gap-2">
|
||||||
|
{/* 차트 타입 전환 드롭다운 (차트일 경우만) */}
|
||||||
|
{element.type === "chart" && (
|
||||||
|
<Select
|
||||||
|
value={element.subtype}
|
||||||
|
onValueChange={(newSubtype: string) => {
|
||||||
|
onUpdate(element.id, { subtype: newSubtype as ElementSubtype });
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<SelectTrigger
|
||||||
|
className="h-7 w-[140px] text-xs"
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
onMouseDown={(e) => e.stopPropagation()}
|
||||||
|
>
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent className="z-[99999]" onClick={(e) => e.stopPropagation()}>
|
||||||
|
{getChartCategory(element.subtype) === "axis-based" ? (
|
||||||
|
<SelectGroup>
|
||||||
|
<SelectLabel>축 기반 차트</SelectLabel>
|
||||||
|
<SelectItem value="bar">바 차트</SelectItem>
|
||||||
|
<SelectItem value="horizontal-bar">수평 바 차트</SelectItem>
|
||||||
|
<SelectItem value="stacked-bar">누적 바 차트</SelectItem>
|
||||||
|
<SelectItem value="line">꺾은선 차트</SelectItem>
|
||||||
|
<SelectItem value="area">영역 차트</SelectItem>
|
||||||
|
<SelectItem value="combo">콤보 차트</SelectItem>
|
||||||
|
</SelectGroup>
|
||||||
|
) : (
|
||||||
|
<SelectGroup>
|
||||||
|
<SelectLabel>원형 차트</SelectLabel>
|
||||||
|
<SelectItem value="pie">원형 차트</SelectItem>
|
||||||
|
<SelectItem value="donut">도넛 차트</SelectItem>
|
||||||
|
</SelectGroup>
|
||||||
|
)}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
)}
|
||||||
|
{/* 제목 */}
|
||||||
|
{!element.type || element.type !== "chart" ? (
|
||||||
|
<span className="text-sm font-bold text-gray-800">{element.customTitle || element.title}</span>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
<div className="flex gap-1">
|
<div className="flex gap-1">
|
||||||
{/* 삭제 버튼 */}
|
{/* 삭제 버튼 */}
|
||||||
<Button
|
<Button
|
||||||
|
|
@ -725,6 +782,7 @@ export function CanvasElement({
|
||||||
size="icon"
|
size="icon"
|
||||||
className="element-close hover:bg-destructive h-6 w-6 text-gray-400 hover:text-white"
|
className="element-close hover:bg-destructive h-6 w-6 text-gray-400 hover:text-white"
|
||||||
onClick={handleRemove}
|
onClick={handleRemove}
|
||||||
|
onMouseDown={(e) => e.stopPropagation()}
|
||||||
title="삭제"
|
title="삭제"
|
||||||
>
|
>
|
||||||
<X className="h-4 w-4" />
|
<X className="h-4 w-4" />
|
||||||
|
|
@ -733,7 +791,7 @@ export function CanvasElement({
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 내용 */}
|
{/* 내용 */}
|
||||||
<div className="relative h-[calc(100%-45px)]">
|
<div className="relative h-[calc(100%-50px)] px-4 pb-4">
|
||||||
{element.type === "chart" ? (
|
{element.type === "chart" ? (
|
||||||
// 차트 렌더링
|
// 차트 렌더링
|
||||||
<div className="h-full w-full bg-white">
|
<div className="h-full w-full bg-white">
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import React, { useState, useCallback, useEffect } from "react";
|
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 { Input } from "@/components/ui/input";
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
|
|
@ -40,8 +40,8 @@ export function ChartConfigPanel({
|
||||||
const [currentConfig, setCurrentConfig] = useState<ChartConfig>(config || {});
|
const [currentConfig, setCurrentConfig] = useState<ChartConfig>(config || {});
|
||||||
const [dateColumns, setDateColumns] = useState<string[]>([]);
|
const [dateColumns, setDateColumns] = useState<string[]>([]);
|
||||||
|
|
||||||
// 원형/도넛 차트 또는 REST API는 Y축이 필수가 아님
|
// 원형 차트 또는 REST API는 Y축이 필수가 아님
|
||||||
const isPieChart = chartType === "pie" || chartType === "donut";
|
const isPieChart = chartType ? isCircularChart(chartType as any) : false;
|
||||||
const isApiSource = dataSourceType === "api";
|
const isApiSource = dataSourceType === "api";
|
||||||
|
|
||||||
// 설정 업데이트
|
// 설정 업데이트
|
||||||
|
|
|
||||||
|
|
@ -159,15 +159,18 @@ export function DashboardTopMenu({
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent className="z-[99999]">
|
<SelectContent className="z-[99999]">
|
||||||
<SelectGroup>
|
<SelectGroup>
|
||||||
<SelectLabel>차트</SelectLabel>
|
<SelectLabel>축 기반 차트</SelectLabel>
|
||||||
<SelectItem value="bar">바 차트</SelectItem>
|
<SelectItem value="bar">바 차트</SelectItem>
|
||||||
<SelectItem value="horizontal-bar">수평 바 차트</SelectItem>
|
<SelectItem value="horizontal-bar">수평 바 차트</SelectItem>
|
||||||
<SelectItem value="stacked-bar">누적 바 차트</SelectItem>
|
<SelectItem value="stacked-bar">누적 바 차트</SelectItem>
|
||||||
<SelectItem value="line">꺾은선 차트</SelectItem>
|
<SelectItem value="line">꺾은선 차트</SelectItem>
|
||||||
<SelectItem value="area">영역 차트</SelectItem>
|
<SelectItem value="area">영역 차트</SelectItem>
|
||||||
|
<SelectItem value="combo">콤보 차트</SelectItem>
|
||||||
|
</SelectGroup>
|
||||||
|
<SelectGroup>
|
||||||
|
<SelectLabel>원형 차트</SelectLabel>
|
||||||
<SelectItem value="pie">원형 차트</SelectItem>
|
<SelectItem value="pie">원형 차트</SelectItem>
|
||||||
<SelectItem value="donut">도넛 차트</SelectItem>
|
<SelectItem value="donut">도넛 차트</SelectItem>
|
||||||
<SelectItem value="combo">콤보 차트</SelectItem>
|
|
||||||
</SelectGroup>
|
</SelectGroup>
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
|
|
|
||||||
|
|
@ -42,6 +42,40 @@ export type ElementSubtype =
|
||||||
| "transport-stats" // 커스텀 통계 카드 위젯
|
| "transport-stats" // 커스텀 통계 카드 위젯
|
||||||
| "custom-metric"; // 사용자 커스텀 카드 위젯
|
| "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 {
|
export interface Position {
|
||||||
x: number;
|
x: number;
|
||||||
y: number;
|
y: number;
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue