ERP-node/frontend/lib/registry/pop-components/pop-dashboard/items/GaugeItem.tsx

142 lines
4.0 KiB
TypeScript

"use client";
/**
* 게이지 서브타입 컴포넌트
*
* SVG 기반 반원형 게이지 (외부 라이브러리 불필요)
* min/max/target/current 표시, 달성률 구간별 색상
*/
import React from "react";
import type { DashboardItem } from "../../types";
import { abbreviateNumber } from "../utils/formula";
// ===== Props =====
export interface GaugeItemProps {
item: DashboardItem;
data: number | null;
/** 동적 목표값 (targetDataSource로 조회된 값) */
targetValue?: number | null;
}
// ===== 게이지 색상 판정 =====
function getGaugeColor(
percentage: number,
ranges?: { min: number; max: number; color: string }[]
): string {
if (ranges?.length) {
const match = ranges.find((r) => percentage >= r.min && percentage <= r.max);
if (match) return match.color;
}
// 기본 색상 (달성률 기준)
if (percentage >= 80) return "#10b981"; // emerald
if (percentage >= 50) return "#f59e0b"; // amber
return "#ef4444"; // rose
}
// ===== 메인 컴포넌트 =====
export function GaugeItemComponent({
item,
data,
targetValue,
}: GaugeItemProps) {
const { visibility, gaugeConfig } = item;
const current = data ?? 0;
const min = gaugeConfig?.min ?? 0;
const max = gaugeConfig?.max ?? 100;
const target = targetValue ?? gaugeConfig?.target ?? max;
// 달성률 계산 (0~100)
const range = max - min;
const percentage = range > 0 ? Math.min(100, Math.max(0, ((current - min) / range) * 100)) : 0;
const gaugeColor = getGaugeColor(percentage, gaugeConfig?.colorRanges);
// SVG 반원 게이지 수치
const cx = 100;
const cy = 90;
const radius = 70;
// 반원: 180도 -> percentage에 비례한 각도
const startAngle = Math.PI; // 180도 (왼쪽)
const endAngle = Math.PI - (percentage / 100) * Math.PI; // 0도 (오른쪽) 방향
const startX = cx + radius * Math.cos(startAngle);
const startY = cy - radius * Math.sin(startAngle);
const endX = cx + radius * Math.cos(endAngle);
const endY = cy - radius * Math.sin(endAngle);
const largeArcFlag = percentage > 50 ? 1 : 0;
return (
<div className="@container flex h-full w-full flex-col items-center justify-center p-2">
{/* 라벨 */}
{visibility.showLabel && (
<p className="shrink-0 truncate text-xs text-muted-foreground @[250px]:text-sm">
{item.label}
</p>
)}
{/* 게이지 SVG - 높이/너비 모두 반응형 */}
<div className="flex min-h-0 flex-1 items-center justify-center w-full">
<svg
viewBox="0 0 200 110"
className="h-full w-auto max-w-full"
preserveAspectRatio="xMidYMid meet"
>
{/* 배경 반원 (회색) */}
<path
d={`M ${cx - radius} ${cy} A ${radius} ${radius} 0 0 1 ${cx + radius} ${cy}`}
fill="none"
stroke="#e5e7eb"
strokeWidth="12"
strokeLinecap="round"
/>
{/* 값 반원 (색상) */}
{percentage > 0 && (
<path
d={`M ${startX} ${startY} A ${radius} ${radius} 0 ${largeArcFlag} 1 ${endX} ${endY}`}
fill="none"
stroke={gaugeColor}
strokeWidth="12"
strokeLinecap="round"
/>
)}
{/* 중앙 텍스트 */}
{visibility.showValue && (
<text
x={cx}
y={cy - 10}
textAnchor="middle"
className="fill-foreground text-2xl font-bold"
fontSize="24"
>
{abbreviateNumber(current)}
</text>
)}
{/* 퍼센트 */}
<text
x={cx}
y={cy + 10}
textAnchor="middle"
className="fill-muted-foreground text-xs"
fontSize="12"
>
{percentage.toFixed(1)}%
</text>
</svg>
</div>
{/* 목표값 */}
{visibility.showTarget && (
<p className="shrink-0 text-xs text-muted-foreground">
: {abbreviateNumber(target)}
</p>
)}
</div>
);
}