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

114 lines
3.2 KiB
TypeScript

"use client";
/**
* KPI 카드 서브타입 컴포넌트
*
* 큰 숫자 + 단위 + 증감 표시
* CSS Container Query로 반응형 내부 콘텐츠
*/
import React from "react";
import type { DashboardItem } from "../../types";
import { TEXT_ALIGN_CLASSES } from "../../types";
import { abbreviateNumber } from "../utils/formula";
// ===== Props =====
export interface KpiCardProps {
item: DashboardItem;
data: number | null;
/** 이전 기간 대비 증감 퍼센트 (선택) */
trendValue?: number | null;
/** 수식 결과 표시 문자열 (formula가 있을 때) */
formulaDisplay?: string | null;
}
// ===== 증감 표시 =====
function TrendIndicator({ value }: { value: number }) {
const isPositive = value > 0;
const isZero = value === 0;
const color = isPositive
? "text-emerald-600"
: isZero
? "text-muted-foreground"
: "text-rose-600";
const arrow = isPositive ? "↑" : isZero ? "→" : "↓";
return (
<span className={`inline-flex items-center gap-0.5 text-xs font-medium ${color}`}>
<span>{arrow}</span>
<span>{Math.abs(value).toFixed(1)}%</span>
</span>
);
}
// ===== 색상 구간 판정 =====
function getColorForValue(
value: number,
ranges?: { min: number; max: number; color: string }[]
): string | undefined {
if (!ranges?.length) return undefined;
const match = ranges.find((r) => value >= r.min && value <= r.max);
return match?.color;
}
// ===== 메인 컴포넌트 =====
export function KpiCardComponent({
item,
data,
trendValue,
formulaDisplay,
}: KpiCardProps) {
const { visibility, kpiConfig, itemStyle } = item;
const displayValue = data ?? 0;
const valueColor = getColorForValue(displayValue, kpiConfig?.colorRanges);
// 라벨 정렬만 사용자 설정, 나머지는 @container 반응형 자동
const labelAlignClass = TEXT_ALIGN_CLASSES[itemStyle?.labelAlign ?? "center"];
return (
<div className="@container flex h-full w-full flex-col items-center justify-center p-3">
{/* 라벨 - 사용자 정렬 적용 */}
{visibility.showLabel && (
<p className={`w-full text-muted-foreground text-xs @[250px]:text-sm ${labelAlignClass}`}>
{item.label}
</p>
)}
{/* 메인 값 - @container 반응형 */}
{visibility.showValue && (
<div className="flex items-baseline gap-1">
<span
className="text-xl font-bold @[200px]:text-3xl @[350px]:text-4xl @[500px]:text-5xl"
style={valueColor ? { color: valueColor } : undefined}
>
{formulaDisplay ?? abbreviateNumber(displayValue)}
</span>
{/* 단위 */}
{visibility.showUnit && kpiConfig?.unit && (
<span className="text-xs text-muted-foreground @[200px]:text-sm">
{kpiConfig.unit}
</span>
)}
</div>
)}
{/* 증감율 */}
{visibility.showTrend && trendValue != null && (
<TrendIndicator value={trendValue} />
)}
{/* 보조 라벨 (수식 표시 등) */}
{visibility.showSubLabel && formulaDisplay && (
<p className="text-xs text-muted-foreground @[200px]:text-sm">
{item.formula?.values.map((v) => v.label).join(" / ")}
</p>
)}
</div>
);
}