114 lines
3.2 KiB
TypeScript
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>
|
|
);
|
|
}
|