- 플랫폼 관리 API 연동 (FCM/APNs 인증서 등록/삭제) - 서비스 상세 통계 카드 대시보드 KPI API 연결 - 통계 서브텍스트 전일 대비 변동 데이터 연결 (변동 없음 표시 포함) - ServiceHeaderCard/ServiceEditPage optional chaining 버그 수정 - 날짜 표기 YYYY-MM-DD 형식 통일 - 페이지네이션 totalCount 필드명 수정 및 패딩 정렬 - 서비스 등록 완료 모달 UI 통일 및 문구 수정 Closes #31
134 lines
4.4 KiB
TypeScript
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>
|
|
);
|
|
}
|