Merge pull request '대시보드 기타 수정사항' (#156) from feat/dashboard into main
Reviewed-on: http://39.117.244.52:3000/kjs/ERP-node/pulls/156
This commit is contained in:
commit
bf809e729b
|
|
@ -353,9 +353,9 @@ export function CanvasElement({
|
||||||
let newX = resizeStart.elementX;
|
let newX = resizeStart.elementX;
|
||||||
let newY = resizeStart.elementY;
|
let newY = resizeStart.elementY;
|
||||||
|
|
||||||
// 최소 크기 설정: 달력은 2x3, 나머지는 2x2
|
// 최소 크기 설정: 모든 위젯 1x1
|
||||||
const minWidthCells = 2;
|
const minWidthCells = 1;
|
||||||
const minHeightCells = element.type === "widget" && element.subtype === "calendar" ? 3 : 2;
|
const minHeightCells = 1;
|
||||||
const minWidth = cellSize * minWidthCells;
|
const minWidth = cellSize * minWidthCells;
|
||||||
const minHeight = cellSize * minHeightCells;
|
const minHeight = cellSize * minHeightCells;
|
||||||
|
|
||||||
|
|
@ -721,7 +721,7 @@ export function CanvasElement({
|
||||||
<div
|
<div
|
||||||
ref={elementRef}
|
ref={elementRef}
|
||||||
data-element-id={element.id}
|
data-element-id={element.id}
|
||||||
className={`absolute min-h-[120px] min-w-[120px] cursor-move rounded-lg border-2 bg-white shadow-lg ${isSelected ? "border-blue-500 shadow-blue-200" : "border-gray-400"} ${isDragging || isResizing ? "transition-none" : "transition-all duration-150"} `}
|
className={`absolute min-h-[120px] min-w-[120px] cursor-move overflow-hidden rounded-lg border-2 bg-white shadow-lg ${isSelected ? "border-blue-500 shadow-blue-200" : "border-gray-400"} ${isDragging || isResizing ? "transition-none" : "transition-all duration-150"} `}
|
||||||
style={{
|
style={{
|
||||||
left: displayPosition.x,
|
left: displayPosition.x,
|
||||||
top: displayPosition.y,
|
top: displayPosition.y,
|
||||||
|
|
@ -732,7 +732,7 @@ export function CanvasElement({
|
||||||
onMouseDown={handleMouseDown}
|
onMouseDown={handleMouseDown}
|
||||||
>
|
>
|
||||||
{/* 헤더 */}
|
{/* 헤더 */}
|
||||||
<div className="flex cursor-move items-center justify-between px-4 py-2">
|
<div className="flex cursor-move items-center justify-between px-2 py-1">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
{/* 차트 타입 전환 드롭다운 (차트일 경우만) */}
|
{/* 차트 타입 전환 드롭다운 (차트일 경우만) */}
|
||||||
{element.type === "chart" && (
|
{element.type === "chart" && (
|
||||||
|
|
@ -743,7 +743,7 @@ export function CanvasElement({
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<SelectTrigger
|
<SelectTrigger
|
||||||
className="h-7 w-[140px] text-xs"
|
className="h-6 w-[120px] text-[11px]"
|
||||||
onClick={(e) => e.stopPropagation()}
|
onClick={(e) => e.stopPropagation()}
|
||||||
onMouseDown={(e) => e.stopPropagation()}
|
onMouseDown={(e) => e.stopPropagation()}
|
||||||
>
|
>
|
||||||
|
|
@ -772,7 +772,7 @@ export function CanvasElement({
|
||||||
)}
|
)}
|
||||||
{/* 제목 */}
|
{/* 제목 */}
|
||||||
{!element.type || element.type !== "chart" ? (
|
{!element.type || element.type !== "chart" ? (
|
||||||
<span className="text-sm font-bold text-gray-800">{element.customTitle || element.title}</span>
|
<span className="text-xs font-bold text-gray-800">{element.customTitle || element.title}</span>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex gap-1">
|
<div className="flex gap-1">
|
||||||
|
|
@ -780,18 +780,18 @@ export function CanvasElement({
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
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-5 w-5 text-gray-400 hover:text-white"
|
||||||
onClick={handleRemove}
|
onClick={handleRemove}
|
||||||
onMouseDown={(e) => e.stopPropagation()}
|
onMouseDown={(e) => e.stopPropagation()}
|
||||||
title="삭제"
|
title="삭제"
|
||||||
>
|
>
|
||||||
<X className="h-4 w-4" />
|
<X className="h-3 w-3" />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 내용 */}
|
{/* 내용 */}
|
||||||
<div className="relative h-[calc(100%-50px)] px-4 pb-4">
|
<div className="relative h-[calc(100%-32px)] px-2 pb-2">
|
||||||
{element.type === "chart" ? (
|
{element.type === "chart" ? (
|
||||||
// 차트 렌더링
|
// 차트 렌더링
|
||||||
<div className="h-full w-full bg-white">
|
<div className="h-full w-full bg-white">
|
||||||
|
|
@ -807,7 +807,7 @@ export function CanvasElement({
|
||||||
element={element}
|
element={element}
|
||||||
data={chartData || undefined}
|
data={chartData || undefined}
|
||||||
width={element.size.width}
|
width={element.size.width}
|
||||||
height={element.size.height - 45}
|
height={element.size.height - 32}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -271,6 +271,10 @@ export default function DashboardDesigner({ dashboardId: initialDashboardId }: D
|
||||||
setElements((prev) => [...prev, newElement]);
|
setElements((prev) => [...prev, newElement]);
|
||||||
setElementCounter((prev) => prev + 1);
|
setElementCounter((prev) => prev + 1);
|
||||||
setSelectedElement(newElement.id);
|
setSelectedElement(newElement.id);
|
||||||
|
|
||||||
|
// 새 요소 생성 시 자동으로 설정 사이드바 열기
|
||||||
|
setSidebarElement(newElement);
|
||||||
|
setSidebarOpen(true);
|
||||||
},
|
},
|
||||||
[elementCounter, canvasConfig],
|
[elementCounter, canvasConfig],
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -242,12 +242,12 @@ export function ChartRenderer({ element, data, width, height = 200 }: ChartRende
|
||||||
// D3 차트 렌더링
|
// D3 차트 렌더링
|
||||||
const actualWidth = width !== undefined ? width : containerWidth;
|
const actualWidth = width !== undefined ? width : containerWidth;
|
||||||
|
|
||||||
// 원형 차트는 더 큰 크기가 필요 (최소 400px)
|
// 최소 크기 제약 완화 (1x1 위젯 지원)
|
||||||
const isCircularChart = element.subtype === "pie" || element.subtype === "donut";
|
const isCircularChart = element.subtype === "pie" || element.subtype === "donut";
|
||||||
const minWidth = isCircularChart ? 400 : 200;
|
const minWidth = 35; // 최소 너비 35px
|
||||||
const finalWidth = Math.max(actualWidth - 20, minWidth);
|
const finalWidth = Math.max(actualWidth - 4, minWidth);
|
||||||
// 원형 차트는 범례 공간을 위해 더 많은 여백 필요
|
// 최소 높이도 35px로 설정
|
||||||
const finalHeight = Math.max(height - (isCircularChart ? 60 : 20), 300);
|
const finalHeight = Math.max(height - (isCircularChart ? 10 : 4), 35);
|
||||||
|
|
||||||
console.log("🎨 ChartRenderer:", {
|
console.log("🎨 ChartRenderer:", {
|
||||||
elementSubtype: element.subtype,
|
elementSubtype: element.subtype,
|
||||||
|
|
@ -263,7 +263,7 @@ export function ChartRenderer({ element, data, width, height = 200 }: ChartRende
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div ref={containerRef} className="flex h-full w-full items-center justify-center bg-white p-2">
|
<div ref={containerRef} className="flex h-full w-full items-center justify-center bg-white p-0.5">
|
||||||
<div className="flex items-center justify-center">
|
<div className="flex items-center justify-center">
|
||||||
<Chart
|
<Chart
|
||||||
chartType={element.subtype}
|
chartType={element.subtype}
|
||||||
|
|
|
||||||
|
|
@ -169,7 +169,7 @@ export function PieChart({ data, config, width = 500, height = 500, isDonut = fa
|
||||||
.attr("x", 0)
|
.attr("x", 0)
|
||||||
.attr("y", 18)
|
.attr("y", 18)
|
||||||
.attr("text-anchor", "middle") // 텍스트 중앙 정렬
|
.attr("text-anchor", "middle") // 텍스트 중앙 정렬
|
||||||
.style("font-size", "10px")
|
.style("font-size", "8px")
|
||||||
.style("fill", "#333")
|
.style("fill", "#333")
|
||||||
.text(`${d.label} (${d.value})`);
|
.text(`${d.label} (${d.value})`);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -31,8 +31,8 @@ export function AnalogClock({ time, theme, timezone, customColor }: AnalogClockP
|
||||||
const timezoneLabel = timezone ? getTimezoneLabel(timezone) : "";
|
const timezoneLabel = timezone ? getTimezoneLabel(timezone) : "";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex h-full flex-col items-center justify-center p-2">
|
<div className="flex h-full flex-col items-center justify-center p-0.5">
|
||||||
<svg viewBox="0 0 200 200" className="h-full max-h-[200px] w-full max-w-[200px]">
|
<svg viewBox="0 0 200 200" className="h-full w-full">
|
||||||
{/* 시계판 배경 */}
|
{/* 시계판 배경 */}
|
||||||
<circle cx="100" cy="100" r="98" fill={colors.background} stroke={colors.border} strokeWidth="2" />
|
<circle cx="100" cy="100" r="98" fill={colors.background} stroke={colors.border} strokeWidth="2" />
|
||||||
|
|
||||||
|
|
@ -70,7 +70,7 @@ export function AnalogClock({ time, theme, timezone, customColor }: AnalogClockP
|
||||||
y={y}
|
y={y}
|
||||||
textAnchor="middle"
|
textAnchor="middle"
|
||||||
dominantBaseline="middle"
|
dominantBaseline="middle"
|
||||||
fontSize="20"
|
fontSize="16"
|
||||||
fontWeight="bold"
|
fontWeight="bold"
|
||||||
fill={colors.number}
|
fill={colors.number}
|
||||||
>
|
>
|
||||||
|
|
@ -86,7 +86,7 @@ export function AnalogClock({ time, theme, timezone, customColor }: AnalogClockP
|
||||||
x2={100 + 40 * Math.cos((hourAngle * Math.PI) / 180)}
|
x2={100 + 40 * Math.cos((hourAngle * Math.PI) / 180)}
|
||||||
y2={100 + 40 * Math.sin((hourAngle * Math.PI) / 180)}
|
y2={100 + 40 * Math.sin((hourAngle * Math.PI) / 180)}
|
||||||
stroke={colors.hourHand}
|
stroke={colors.hourHand}
|
||||||
strokeWidth="6"
|
strokeWidth="5"
|
||||||
strokeLinecap="round"
|
strokeLinecap="round"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|
@ -97,7 +97,7 @@ export function AnalogClock({ time, theme, timezone, customColor }: AnalogClockP
|
||||||
x2={100 + 60 * Math.cos((minuteAngle * Math.PI) / 180)}
|
x2={100 + 60 * Math.cos((minuteAngle * Math.PI) / 180)}
|
||||||
y2={100 + 60 * Math.sin((minuteAngle * Math.PI) / 180)}
|
y2={100 + 60 * Math.sin((minuteAngle * Math.PI) / 180)}
|
||||||
stroke={colors.minuteHand}
|
stroke={colors.minuteHand}
|
||||||
strokeWidth="4"
|
strokeWidth="3"
|
||||||
strokeLinecap="round"
|
strokeLinecap="round"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|
@ -108,18 +108,18 @@ export function AnalogClock({ time, theme, timezone, customColor }: AnalogClockP
|
||||||
x2={100 + 75 * Math.cos((secondAngle * Math.PI) / 180)}
|
x2={100 + 75 * Math.cos((secondAngle * Math.PI) / 180)}
|
||||||
y2={100 + 75 * Math.sin((secondAngle * Math.PI) / 180)}
|
y2={100 + 75 * Math.sin((secondAngle * Math.PI) / 180)}
|
||||||
stroke={colors.secondHand}
|
stroke={colors.secondHand}
|
||||||
strokeWidth="2"
|
strokeWidth="1.5"
|
||||||
strokeLinecap="round"
|
strokeLinecap="round"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* 중심점 */}
|
{/* 중심점 */}
|
||||||
<circle cx="100" cy="100" r="6" fill={colors.center} />
|
<circle cx="100" cy="100" r="4" fill={colors.center} />
|
||||||
<circle cx="100" cy="100" r="3" fill={colors.background} />
|
<circle cx="100" cy="100" r="2" fill={colors.background} />
|
||||||
</svg>
|
</svg>
|
||||||
|
|
||||||
{/* 타임존 표시 */}
|
{/* 타임존 표시 */}
|
||||||
{timezoneLabel && (
|
{timezoneLabel && (
|
||||||
<div className="mt-1 text-center text-xs font-medium" style={{ color: colors.number }}>
|
<div className="mt-0 text-center text-[8px] font-medium" style={{ color: colors.number }}>
|
||||||
{timezoneLabel}
|
{timezoneLabel}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -56,21 +56,21 @@ export function DigitalClock({
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={`flex h-full flex-col items-center justify-center ${compact ? "p-1" : "p-4"} text-center ${themeClasses.container}`}
|
className={`flex h-full flex-col items-center justify-center ${compact ? "p-0.5" : "p-2"} text-center ${themeClasses.container}`}
|
||||||
style={themeClasses.style}
|
style={themeClasses.style}
|
||||||
>
|
>
|
||||||
{/* 날짜 표시 (compact 모드에서는 숨김) */}
|
{/* 날짜 표시 (compact 모드에서는 숨김) */}
|
||||||
{!compact && showDate && dateString && (
|
{!compact && showDate && dateString && (
|
||||||
<div className={`mb-3 text-sm font-medium ${themeClasses.date}`}>{dateString}</div>
|
<div className={`mb-1 text-[10px] leading-tight font-medium ${themeClasses.date}`}>{dateString}</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* 시간 표시 */}
|
{/* 시간 표시 */}
|
||||||
<div className={`font-bold tabular-nums ${themeClasses.time} ${compact ? "text-xl" : "text-5xl"}`}>
|
<div className={`leading-none font-bold tabular-nums ${themeClasses.time} ${compact ? "text-sm" : "text-2xl"}`}>
|
||||||
{timeString}
|
{timeString}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 타임존 표시 */}
|
{/* 타임존 표시 */}
|
||||||
<div className={`${compact ? "mt-0.5" : "mt-3"} text-xs font-medium ${themeClasses.timezone}`}>
|
<div className={`${compact ? "mt-0" : "mt-1"} text-[9px] font-medium ${themeClasses.timezone}`}>
|
||||||
{timezoneLabel}
|
{timezoneLabel}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -94,24 +94,19 @@ export function ListWidgetConfigSidebar({ element, isOpen, onClose, onApply }: L
|
||||||
const handleQueryTest = useCallback((result: QueryResult) => {
|
const handleQueryTest = useCallback((result: QueryResult) => {
|
||||||
setQueryResult(result);
|
setQueryResult(result);
|
||||||
|
|
||||||
// 쿼리 결과의 컬럼을 자동으로 listConfig.columns에 추가 (기존 컬럼은 유지)
|
// 쿼리 실행 시마다 컬럼 설정 초기화 (새로운 쿼리 결과로 덮어쓰기)
|
||||||
setListConfig((prev) => {
|
const newColumns = result.columns.map((col, idx) => ({
|
||||||
const existingFields = prev.columns.map((col) => col.field);
|
id: `col_${Date.now()}_${idx}`,
|
||||||
const newColumns = result.columns
|
field: col,
|
||||||
.filter((col) => !existingFields.includes(col))
|
label: col,
|
||||||
.map((col, idx) => ({
|
visible: true,
|
||||||
id: `col_${Date.now()}_${idx}`,
|
align: "left" as const,
|
||||||
field: col,
|
}));
|
||||||
label: col,
|
|
||||||
visible: true,
|
|
||||||
align: "left" as const,
|
|
||||||
}));
|
|
||||||
|
|
||||||
return {
|
setListConfig((prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
columns: [...prev.columns, ...newColumns],
|
columns: newColumns,
|
||||||
};
|
}));
|
||||||
});
|
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// 컬럼 설정 변경
|
// 컬럼 설정 변경
|
||||||
|
|
|
||||||
|
|
@ -379,8 +379,8 @@ function ViewerElement({ element, data, isLoading, onRefresh, isMobile, canvasWi
|
||||||
style={{ minHeight: "300px" }}
|
style={{ minHeight: "300px" }}
|
||||||
>
|
>
|
||||||
{element.showHeader !== false && (
|
{element.showHeader !== false && (
|
||||||
<div className="flex items-center justify-between border-b border-gray-200 bg-gray-50 px-4 py-3">
|
<div className="flex items-center justify-between px-2 py-1">
|
||||||
<h3 className="text-sm font-semibold text-gray-800">{element.customTitle || element.title}</h3>
|
<h3 className="text-xs font-semibold text-gray-800">{element.customTitle || element.title}</h3>
|
||||||
<button
|
<button
|
||||||
onClick={onRefresh}
|
onClick={onRefresh}
|
||||||
disabled={isLoading}
|
disabled={isLoading}
|
||||||
|
|
@ -388,7 +388,7 @@ function ViewerElement({ element, data, isLoading, onRefresh, isMobile, canvasWi
|
||||||
title="새로고침"
|
title="새로고침"
|
||||||
>
|
>
|
||||||
<svg
|
<svg
|
||||||
className={`h-4 w-4 ${isLoading ? "animate-spin" : ""}`}
|
className={`h-3 w-3 ${isLoading ? "animate-spin" : ""}`}
|
||||||
fill="none"
|
fill="none"
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
stroke="currentColor"
|
stroke="currentColor"
|
||||||
|
|
@ -403,7 +403,7 @@ function ViewerElement({ element, data, isLoading, onRefresh, isMobile, canvasWi
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className={element.showHeader !== false ? "p-4" : "p-4"} style={{ minHeight: "250px" }}>
|
<div className={element.showHeader !== false ? "p-2" : "p-2"} style={{ minHeight: "250px" }}>
|
||||||
{!isMounted ? (
|
{!isMounted ? (
|
||||||
<div className="flex h-full w-full items-center justify-center">
|
<div className="flex h-full w-full items-center justify-center">
|
||||||
<div className="h-6 w-6 animate-spin rounded-full border-2 border-blue-500 border-t-transparent" />
|
<div className="h-6 w-6 animate-spin rounded-full border-2 border-blue-500 border-t-transparent" />
|
||||||
|
|
@ -441,8 +441,8 @@ function ViewerElement({ element, data, isLoading, onRefresh, isMobile, canvasWi
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{element.showHeader !== false && (
|
{element.showHeader !== false && (
|
||||||
<div className="flex items-center justify-between border-b border-gray-200 bg-gray-50 px-4 py-3">
|
<div className="flex items-center justify-between px-2 py-1">
|
||||||
<h3 className="text-sm font-semibold text-gray-800">{element.customTitle || element.title}</h3>
|
<h3 className="text-xs font-semibold text-gray-800">{element.customTitle || element.title}</h3>
|
||||||
<button
|
<button
|
||||||
onClick={onRefresh}
|
onClick={onRefresh}
|
||||||
disabled={isLoading}
|
disabled={isLoading}
|
||||||
|
|
@ -450,7 +450,7 @@ function ViewerElement({ element, data, isLoading, onRefresh, isMobile, canvasWi
|
||||||
title="새로고침"
|
title="새로고침"
|
||||||
>
|
>
|
||||||
<svg
|
<svg
|
||||||
className={`h-4 w-4 ${isLoading ? "animate-spin" : ""}`}
|
className={`h-3 w-3 ${isLoading ? "animate-spin" : ""}`}
|
||||||
fill="none"
|
fill="none"
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
stroke="currentColor"
|
stroke="currentColor"
|
||||||
|
|
@ -465,7 +465,7 @@ function ViewerElement({ element, data, isLoading, onRefresh, isMobile, canvasWi
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className={element.showHeader !== false ? "h-[calc(100%-50px)] w-full" : "h-full w-full"}>
|
<div className={element.showHeader !== false ? "h-[calc(100%-32px)] w-full" : "h-full w-full"}>
|
||||||
{!isMounted ? (
|
{!isMounted ? (
|
||||||
<div className="flex h-full w-full items-center justify-center">
|
<div className="flex h-full w-full items-center justify-center">
|
||||||
<div className="h-6 w-6 animate-spin rounded-full border-2 border-blue-500 border-t-transparent" />
|
<div className="h-6 w-6 animate-spin rounded-full border-2 border-blue-500 border-t-transparent" />
|
||||||
|
|
@ -475,7 +475,7 @@ function ViewerElement({ element, data, isLoading, onRefresh, isMobile, canvasWi
|
||||||
element={element}
|
element={element}
|
||||||
data={data}
|
data={data}
|
||||||
width={undefined}
|
width={undefined}
|
||||||
height={element.showHeader !== false ? element.size.height - 50 : element.size.height}
|
height={element.showHeader !== false ? element.size.height - 32 : element.size.height}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
renderWidget(element)
|
renderWidget(element)
|
||||||
|
|
|
||||||
|
|
@ -373,48 +373,53 @@ export default function CustomMetricWidget({ element }: CustomMetricWidgetProps)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 위젯 높이에 따라 레이아웃 결정 (세로 1칸이면 가로, 2칸 이상이면 세로)
|
||||||
|
// 실제 측정된 1칸 높이: 119px
|
||||||
|
const isHorizontalLayout = element?.size?.height && element.size.height <= 130; // 1칸 여유 (119px + 약간의 마진)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex h-full w-full items-center justify-center overflow-hidden bg-white p-2">
|
<div
|
||||||
{/* 콘텐츠 영역 - 스크롤 없이 자동으로 크기 조정 */}
|
className={`flex h-full w-full overflow-hidden bg-white p-0.5 ${
|
||||||
<div className="grid h-full w-full gap-2" style={{ gridTemplateColumns: "repeat(auto-fit, minmax(140px, 1fr))" }}>
|
isHorizontalLayout ? "flex-row gap-0.5" : "flex-col gap-0.5"
|
||||||
{/* 그룹별 카드 (활성화 시) */}
|
}`}
|
||||||
{isGroupByMode &&
|
>
|
||||||
groupedCards.map((card, index) => {
|
{/* 그룹별 카드 (활성화 시) */}
|
||||||
// 색상 순환 (6가지 색상)
|
{isGroupByMode &&
|
||||||
const colorKeys = Object.keys(colorMap) as Array<keyof typeof colorMap>;
|
groupedCards.map((card, index) => {
|
||||||
const colorKey = colorKeys[index % colorKeys.length];
|
// 색상 순환 (6가지 색상)
|
||||||
const colors = colorMap[colorKey];
|
const colorKeys = Object.keys(colorMap) as Array<keyof typeof colorMap>;
|
||||||
|
const colorKey = colorKeys[index % colorKeys.length];
|
||||||
return (
|
const colors = colorMap[colorKey];
|
||||||
<div
|
|
||||||
key={`group-${index}`}
|
|
||||||
className={`flex flex-col items-center justify-center rounded-lg border ${colors.bg} ${colors.border} p-2`}
|
|
||||||
>
|
|
||||||
<div className="text-[10px] text-gray-600">{card.label}</div>
|
|
||||||
<div className={`mt-0.5 text-xl font-bold ${colors.text}`}>{card.value.toLocaleString()}</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
|
|
||||||
{/* 일반 지표 카드 (항상 표시) */}
|
|
||||||
{metrics.map((metric) => {
|
|
||||||
const colors = colorMap[metric.color as keyof typeof colorMap] || colorMap.gray;
|
|
||||||
const formattedValue = metric.calculatedValue.toFixed(metric.decimals);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={metric.id}
|
key={`group-${index}`}
|
||||||
className={`flex flex-col items-center justify-center rounded-lg border ${colors.bg} ${colors.border} p-2`}
|
className={`flex flex-1 flex-col items-center justify-center rounded border ${colors.bg} ${colors.border} p-0.5`}
|
||||||
>
|
>
|
||||||
<div className="text-[10px] text-gray-600">{metric.label}</div>
|
<div className="text-[8px] leading-tight text-gray-600">{card.label}</div>
|
||||||
<div className={`mt-0.5 text-xl font-bold ${colors.text}`}>
|
<div className={`mt-0 text-xs leading-none font-bold ${colors.text}`}>{card.value.toLocaleString()}</div>
|
||||||
{formattedValue}
|
|
||||||
<span className="ml-0.5 text-sm">{metric.unit}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
|
||||||
|
{/* 일반 지표 카드 (항상 표시) */}
|
||||||
|
{metrics.map((metric) => {
|
||||||
|
const colors = colorMap[metric.color as keyof typeof colorMap] || colorMap.gray;
|
||||||
|
const formattedValue = metric.calculatedValue.toFixed(metric.decimals);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={metric.id}
|
||||||
|
className={`flex flex-1 flex-col items-center justify-center rounded border ${colors.bg} ${colors.border} p-0.5`}
|
||||||
|
>
|
||||||
|
<div className="text-[8px] leading-tight text-gray-600">{metric.label}</div>
|
||||||
|
<div className={`mt-0 text-xs leading-none font-bold ${colors.text}`}>
|
||||||
|
{formattedValue}
|
||||||
|
<span className="ml-0 text-[8px]">{metric.unit}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue