SPMS_WEB/react/src/features/service/components/ServiceStatsCards.tsx
SEAN bb4d531d8c feat: 서비스 관리 API 연동 및 UI 개선 (#31)
- 플랫폼 관리 API 연동 (FCM/APNs 인증서 등록/삭제)
- 서비스 상세 통계 카드 대시보드 KPI API 연결
- 통계 서브텍스트 전일 대비 변동 데이터 연결 (변동 없음 표시 포함)
- ServiceHeaderCard/ServiceEditPage optional chaining 버그 수정
- 날짜 표기 YYYY-MM-DD 형식 통일
- 페이지네이션 totalCount 필드명 수정 및 패딩 정렬
- 서비스 등록 완료 모달 UI 통일 및 문구 수정

Closes #31
2026-03-01 10:35:54 +09:00

134 lines
4.4 KiB
TypeScript

interface StatCard {
label: string;
value: string;
sub: { type: "trend" | "stable"; text: string; color?: string };
icon: string;
iconBg: string;
iconColor: string;
}
interface ServiceStatsCardsProps {
totalSent: number;
successRate: number;
deviceCount: number;
todaySent: number;
sentChangeRate?: number;
successRateChange?: number;
deviceCountChange?: number;
todaySentChangeRate?: number;
}
export default function ServiceStatsCards({
totalSent,
successRate,
deviceCount,
todaySent,
sentChangeRate = 0,
successRateChange = 0,
deviceCountChange = 0,
todaySentChangeRate = 0,
}: ServiceStatsCardsProps) {
const noChange: StatCard["sub"] = { type: "stable", text: "변동 없음" };
const cards: StatCard[] = [
{
label: "총 발송 수",
value: totalSent.toLocaleString(),
sub: sentChangeRate === 0
? noChange
: sentChangeRate > 0
? { type: "trend", text: `+${sentChangeRate.toLocaleString()}`, color: "text-indigo-600" }
: { type: "trend", text: `${sentChangeRate.toLocaleString()}`, color: "text-red-500" },
icon: "equalizer",
iconBg: "bg-indigo-50",
iconColor: "text-indigo-600",
},
{
label: "성공률",
value: `${successRate}%`,
sub: successRateChange === 0
? noChange
: successRateChange > 0
? { type: "trend", text: `+${successRateChange.toFixed(1)}%`, color: "text-emerald-600" }
: { type: "trend", text: `${successRateChange.toFixed(1)}%`, color: "text-red-500" },
icon: "check_circle",
iconBg: "bg-emerald-50",
iconColor: "text-emerald-600",
},
{
label: "등록 기기 수",
value: deviceCount.toLocaleString(),
sub: deviceCountChange === 0
? noChange
: deviceCountChange > 0
? { type: "trend", text: `+${deviceCountChange.toLocaleString()} today`, color: "text-amber-600" }
: { type: "trend", text: `${deviceCountChange.toLocaleString()} today`, color: "text-red-500" },
icon: "devices",
iconBg: "bg-amber-50",
iconColor: "text-amber-600",
},
{
label: "오늘 발송",
value: todaySent.toLocaleString(),
sub: todaySentChangeRate === 0
? noChange
: todaySentChangeRate > 0
? { type: "trend", text: `+${todaySentChangeRate.toLocaleString()}`, color: "text-[#2563EB]" }
: { type: "trend", text: `${todaySentChangeRate.toLocaleString()}`, color: "text-red-500" },
icon: "today",
iconBg: "bg-[#2563EB]/5",
iconColor: "text-[#2563EB]",
},
];
return (
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4 mb-8">
{cards.map((card) => (
<div
key={card.label}
className="bg-white border border-gray-200 rounded-lg shadow-sm p-6"
>
<div className="flex items-start justify-between">
<div className="flex-1">
<p className="text-xs text-[#64748b] font-semibold uppercase tracking-wide">
{card.label}
</p>
<p className="text-2xl font-bold text-[#0f172a] mt-2">
{card.value}
</p>
<div className="mt-2">
{card.sub.type === "trend" && (
<p className={`text-xs ${card.sub.color ?? "text-green-600"} font-medium flex items-center gap-1`}>
<span
className="material-symbols-outlined"
style={{ fontSize: "14px" }}
>
{card.sub.text.startsWith("-") ? "trending_down" : "trending_up"}
</span>
<span>{card.sub.text}</span>
</p>
)}
{card.sub.type === "stable" && (
<p className="text-xs text-gray-600 font-medium flex items-center gap-1">
<span className="w-2 h-2 rounded-full bg-gray-400" />
<span>{card.sub.text}</span>
</p>
)}
</div>
</div>
<div
className={`size-12 rounded-lg ${card.iconBg} flex items-center justify-center flex-shrink-0`}
>
<span
className={`material-symbols-outlined ${card.iconColor} text-2xl`}
>
{card.icon}
</span>
</div>
</div>
</div>
))}
</div>
);
}