- 서비스 목록 페이지 (검색/필터/페이지네이션, 행 클릭 → 상세) - 서비스 상세 페이지 (헤더카드/통계/플랫폼 관리 모달) - 서비스 등록 페이지 (서비스명/플랫폼 선택/설명/관련링크) - 서비스 수정 페이지 (상태 토글/메타정보/저장 확인 모달) - 공통 훅 추출 (useShake, useBreadcrumbBack) - 브레드크럼 동적 경로 지원 (/services/:id, /services/:id/edit) - 인증 페이지 useShake 공통 훅 리팩터링 Closes #14
114 lines
3.5 KiB
TypeScript
114 lines
3.5 KiB
TypeScript
interface StatCard {
|
|
label: string;
|
|
value: string;
|
|
sub: { type: "trend" | "stable" | "live"; text: string; color?: string };
|
|
icon: string;
|
|
iconBg: string;
|
|
iconColor: string;
|
|
}
|
|
|
|
interface ServiceStatsCardsProps {
|
|
totalSent: number;
|
|
successRate: number;
|
|
deviceCount: number;
|
|
todaySent: number;
|
|
}
|
|
|
|
export default function ServiceStatsCards({
|
|
totalSent,
|
|
successRate,
|
|
deviceCount,
|
|
todaySent,
|
|
}: ServiceStatsCardsProps) {
|
|
const cards: StatCard[] = [
|
|
{
|
|
label: "총 발송 수",
|
|
value: totalSent.toLocaleString(),
|
|
sub: { type: "trend", text: "+12.5%", color: "text-indigo-600" },
|
|
icon: "equalizer",
|
|
iconBg: "bg-indigo-50",
|
|
iconColor: "text-indigo-600",
|
|
},
|
|
{
|
|
label: "성공률",
|
|
value: `${successRate}%`,
|
|
sub: { type: "stable", text: "Stable" },
|
|
icon: "check_circle",
|
|
iconBg: "bg-emerald-50",
|
|
iconColor: "text-emerald-600",
|
|
},
|
|
{
|
|
label: "등록 기기 수",
|
|
value: deviceCount.toLocaleString(),
|
|
sub: { type: "trend", text: "+82 today", color: "text-amber-600" },
|
|
icon: "devices",
|
|
iconBg: "bg-amber-50",
|
|
iconColor: "text-amber-600",
|
|
},
|
|
{
|
|
label: "오늘 발송",
|
|
value: todaySent.toLocaleString(),
|
|
sub: { type: "live", text: "Live" },
|
|
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" }}
|
|
>
|
|
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>
|
|
)}
|
|
{card.sub.type === "live" && (
|
|
<p className="text-xs text-[#2563EB] font-medium flex items-center gap-1">
|
|
<span className="w-2 h-2 rounded-full bg-[#2563EB] animate-pulse" />
|
|
<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>
|
|
);
|
|
}
|