대시보드 기타 수정사항 적용
This commit is contained in:
@@ -105,7 +105,7 @@ export default function DashboardViewPage({ params }: DashboardViewPageProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="h-screen bg-gray-50">
|
<div className="h-screen">
|
||||||
{/* 대시보드 헤더 - 보기 모드에서는 숨김 */}
|
{/* 대시보드 헤더 - 보기 모드에서는 숨김 */}
|
||||||
{/* <div className="border-b border-gray-200 bg-white px-6 py-4">
|
{/* <div className="border-b border-gray-200 bg-white px-6 py-4">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
|
|||||||
@@ -246,7 +246,8 @@ export function ChartRenderer({ element, data, width, height = 200 }: ChartRende
|
|||||||
const isCircularChart = element.subtype === "pie" || element.subtype === "donut";
|
const isCircularChart = element.subtype === "pie" || element.subtype === "donut";
|
||||||
const minWidth = isCircularChart ? 400 : 200;
|
const minWidth = isCircularChart ? 400 : 200;
|
||||||
const finalWidth = Math.max(actualWidth - 20, minWidth);
|
const finalWidth = Math.max(actualWidth - 20, minWidth);
|
||||||
const finalHeight = Math.max(height - 20, 300);
|
// 원형 차트는 범례 공간을 위해 더 많은 여백 필요
|
||||||
|
const finalHeight = Math.max(height - (isCircularChart ? 60 : 20), 300);
|
||||||
|
|
||||||
console.log("🎨 ChartRenderer:", {
|
console.log("🎨 ChartRenderer:", {
|
||||||
elementSubtype: element.subtype,
|
elementSubtype: element.subtype,
|
||||||
|
|||||||
@@ -24,12 +24,17 @@ export function PieChart({ data, config, width = 500, height = 500, isDonut = fa
|
|||||||
const svg = d3.select(svgRef.current);
|
const svg = d3.select(svgRef.current);
|
||||||
svg.selectAll("*").remove();
|
svg.selectAll("*").remove();
|
||||||
|
|
||||||
const margin = { top: 40, right: 150, bottom: 40, left: 120 };
|
// 범례를 위한 여백 확보 (아래 80px)
|
||||||
|
const legendHeight = config.showLegend !== false ? 80 : 0;
|
||||||
|
const margin = { top: 20, right: 20, bottom: 20 + legendHeight, left: 20 };
|
||||||
const chartWidth = width - margin.left - margin.right;
|
const chartWidth = width - margin.left - margin.right;
|
||||||
const chartHeight = height - margin.top - margin.bottom;
|
const chartHeight = height - margin.top - margin.bottom - legendHeight;
|
||||||
const radius = Math.min(chartWidth, chartHeight) / 2;
|
const radius = Math.min(chartWidth, chartHeight) / 2;
|
||||||
|
|
||||||
const g = svg.append("g").attr("transform", `translate(${width / 2},${height / 2})`);
|
// 차트를 위쪽에 배치 (범례 공간 확보)
|
||||||
|
const centerX = width / 2;
|
||||||
|
const centerY = margin.top + radius + 20;
|
||||||
|
const g = svg.append("g").attr("transform", `translate(${centerX},${centerY})`);
|
||||||
|
|
||||||
// 색상 팔레트
|
// 색상 팔레트
|
||||||
const colors = config.colors || ["#3B82F6", "#EF4444", "#10B981", "#F59E0B", "#8B5CF6", "#EC4899"];
|
const colors = config.colors || ["#3B82F6", "#EF4444", "#10B981", "#F59E0B", "#8B5CF6", "#EC4899"];
|
||||||
@@ -138,10 +143,10 @@ export function PieChart({ data, config, width = 500, height = 500, isDonut = fa
|
|||||||
|
|
||||||
// 범례 (차트 아래, 가로 배치, 중앙 정렬)
|
// 범례 (차트 아래, 가로 배치, 중앙 정렬)
|
||||||
if (config.showLegend !== false) {
|
if (config.showLegend !== false) {
|
||||||
const itemSpacing = 140; // 각 범례 항목 사이 간격
|
const itemSpacing = 100; // 각 범례 항목 사이 간격 (줄임)
|
||||||
const totalWidth = pieData.length * itemSpacing;
|
const totalWidth = pieData.length * itemSpacing;
|
||||||
const legendStartX = (width - totalWidth) / 2; // 시작 위치
|
const legendStartX = (width - totalWidth) / 2; // 시작 위치
|
||||||
const legendY = height - 40; // 차트 아래 (여백 확보)
|
const legendY = centerY + radius + 40; // 차트 아래 40px
|
||||||
|
|
||||||
const legend = svg.append("g").attr("class", "legend");
|
const legend = svg.append("g").attr("class", "legend");
|
||||||
|
|
||||||
@@ -152,19 +157,19 @@ export function PieChart({ data, config, width = 500, height = 500, isDonut = fa
|
|||||||
|
|
||||||
legendItem
|
legendItem
|
||||||
.append("rect")
|
.append("rect")
|
||||||
.attr("x", -7.5) // 사각형을 중앙 기준으로
|
.attr("x", -6) // 사각형을 중앙 기준으로
|
||||||
.attr("y", -7.5)
|
.attr("y", -6)
|
||||||
.attr("width", 15)
|
.attr("width", 12)
|
||||||
.attr("height", 15)
|
.attr("height", 12)
|
||||||
.attr("fill", colors[i % colors.length])
|
.attr("fill", colors[i % colors.length])
|
||||||
.attr("rx", 3);
|
.attr("rx", 2);
|
||||||
|
|
||||||
legendItem
|
legendItem
|
||||||
.append("text")
|
.append("text")
|
||||||
.attr("x", 0)
|
.attr("x", 0)
|
||||||
.attr("y", 20)
|
.attr("y", 18)
|
||||||
.attr("text-anchor", "middle") // 텍스트 중앙 정렬
|
.attr("text-anchor", "middle") // 텍스트 중앙 정렬
|
||||||
.style("font-size", "11px")
|
.style("font-size", "10px")
|
||||||
.style("fill", "#333")
|
.style("fill", "#333")
|
||||||
.text(`${d.label} (${d.value})`);
|
.text(`${d.label} (${d.value})`);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -111,19 +111,21 @@ export function ClockWidget({ element, onConfigUpdate }: ClockWidgetProps) {
|
|||||||
{/* 시계 콘텐츠 */}
|
{/* 시계 콘텐츠 */}
|
||||||
{renderClockContent()}
|
{renderClockContent()}
|
||||||
|
|
||||||
{/* 설정 버튼 - 우측 상단 */}
|
{/* 설정 버튼 - 우측 상단 (디자이너 모드에서만 표시) */}
|
||||||
<div className="absolute top-2 right-2">
|
{onConfigUpdate && (
|
||||||
<Popover open={settingsOpen} onOpenChange={setSettingsOpen}>
|
<div className="absolute top-2 right-2">
|
||||||
<PopoverTrigger asChild>
|
<Popover open={settingsOpen} onOpenChange={setSettingsOpen}>
|
||||||
<Button variant="ghost" size="icon" className="h-8 w-8 bg-white/80 hover:bg-white">
|
<PopoverTrigger asChild>
|
||||||
<Settings className="h-4 w-4" />
|
<Button variant="ghost" size="icon" className="h-8 w-8 bg-white/80 hover:bg-white">
|
||||||
</Button>
|
<Settings className="h-4 w-4" />
|
||||||
</PopoverTrigger>
|
</Button>
|
||||||
<PopoverContent className="w-[500px] p-0" align="end">
|
</PopoverTrigger>
|
||||||
<ClockSettings config={config} onSave={handleSaveSettings} onClose={() => setSettingsOpen(false)} />
|
<PopoverContent className="w-[500px] p-0" align="end">
|
||||||
</PopoverContent>
|
<ClockSettings config={config} onSave={handleSaveSettings} onClose={() => setSettingsOpen(false)} />
|
||||||
</Popover>
|
</PopoverContent>
|
||||||
</div>
|
</Popover>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -216,7 +216,7 @@ export function ListWidget({ element }: ListWidgetProps) {
|
|||||||
const paginatedRows = config.enablePagination ? data.rows.slice(startIdx, endIdx) : data.rows;
|
const paginatedRows = config.enablePagination ? data.rows.slice(startIdx, endIdx) : data.rows;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex h-full w-full flex-col p-4">
|
<div className="flex h-full w-full flex-col gap-3 p-4">
|
||||||
{/* 테이블 뷰 */}
|
{/* 테이블 뷰 */}
|
||||||
{config.viewMode === "table" && (
|
{config.viewMode === "table" && (
|
||||||
<div className={`flex-1 overflow-auto rounded-lg border ${config.compactMode ? "text-xs" : "text-sm"}`}>
|
<div className={`flex-1 overflow-auto rounded-lg border ${config.compactMode ? "text-xs" : "text-sm"}`}>
|
||||||
@@ -306,7 +306,7 @@ export function ListWidget({ element }: ListWidgetProps) {
|
|||||||
|
|
||||||
{/* 페이지네이션 */}
|
{/* 페이지네이션 */}
|
||||||
{config.enablePagination && totalPages > 1 && (
|
{config.enablePagination && totalPages > 1 && (
|
||||||
<div className="mt-4 flex items-center justify-between text-sm">
|
<div className="flex shrink-0 items-center justify-between border-t pt-3 text-sm">
|
||||||
<div className="text-gray-600">
|
<div className="text-gray-600">
|
||||||
{startIdx + 1}-{Math.min(endIdx, data.rows.length)} / {data.rows.length}개
|
{startIdx + 1}-{Math.min(endIdx, data.rows.length)} / {data.rows.length}개
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -374,45 +374,46 @@ export default function CustomMetricWidget({ element }: CustomMetricWidgetProps)
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex h-full w-full flex-col overflow-hidden bg-white p-4">
|
<div className="flex h-full w-full items-center justify-center overflow-hidden bg-white p-4">
|
||||||
{/* 스크롤 가능한 콘텐츠 영역 */}
|
{/* 콘텐츠 영역 - 스크롤 없이 자동으로 크기 조정 */}
|
||||||
<div className="flex-1 overflow-y-auto">
|
<div className="grid h-full w-full gap-3" style={{ gridTemplateColumns: "repeat(auto-fit, minmax(150px, 1fr))" }}>
|
||||||
<div className="grid w-full gap-4" style={{ gridTemplateColumns: "repeat(auto-fit, minmax(180px, 1fr))" }}>
|
{/* 그룹별 카드 (활성화 시) */}
|
||||||
{/* 그룹별 카드 (활성화 시) */}
|
{isGroupByMode &&
|
||||||
{isGroupByMode &&
|
groupedCards.map((card, index) => {
|
||||||
groupedCards.map((card, index) => {
|
// 색상 순환 (6가지 색상)
|
||||||
// 색상 순환 (6가지 색상)
|
const colorKeys = Object.keys(colorMap) as Array<keyof typeof colorMap>;
|
||||||
const colorKeys = Object.keys(colorMap) as Array<keyof typeof colorMap>;
|
const colorKey = colorKeys[index % colorKeys.length];
|
||||||
const colorKey = colorKeys[index % colorKeys.length];
|
const colors = colorMap[colorKey];
|
||||||
const colors = colorMap[colorKey];
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
key={`group-${index}`}
|
|
||||||
className={`rounded-lg border ${colors.bg} ${colors.border} p-4 text-center`}
|
|
||||||
>
|
|
||||||
<div className="text-sm text-gray-600">{card.label}</div>
|
|
||||||
<div className={`mt-2 text-3xl 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 key={metric.id} className={`rounded-lg border ${colors.bg} ${colors.border} p-4 text-center`}>
|
<div
|
||||||
<div className="text-sm text-gray-600">{metric.label}</div>
|
key={`group-${index}`}
|
||||||
<div className={`mt-2 text-3xl font-bold ${colors.text}`}>
|
className={`flex flex-col items-center justify-center rounded-lg border ${colors.bg} ${colors.border} p-3`}
|
||||||
{formattedValue}
|
>
|
||||||
<span className="ml-1 text-lg">{metric.unit}</span>
|
<div className="text-xs text-gray-600">{card.label}</div>
|
||||||
</div>
|
<div className={`mt-1 text-2xl font-bold ${colors.text}`}>{card.value.toLocaleString()}</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-col items-center justify-center rounded-lg border ${colors.bg} ${colors.border} p-3`}
|
||||||
|
>
|
||||||
|
<div className="text-xs text-gray-600">{metric.label}</div>
|
||||||
|
<div className={`mt-1 text-2xl font-bold ${colors.text}`}>
|
||||||
|
{formattedValue}
|
||||||
|
<span className="ml-1 text-base">{metric.unit}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user