대시보드 기타 수정사항 적용

This commit is contained in:
dohyeons
2025-10-27 15:19:48 +09:00
parent d4579e4221
commit 270c322daf
6 changed files with 72 additions and 63 deletions

View File

@@ -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">

View File

@@ -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,

View File

@@ -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})`);
}); });

View File

@@ -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>
); );
} }

View File

@@ -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>

View File

@@ -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>
); );