o2o-clinicad-frontend/src/components/rating/ScoreRing.tsx

88 lines
2.5 KiB
TypeScript

import { useEffect, useState } from "react";
export type ScoreRingProps = {
score: number;
maxScore?: number;
size?: number;
label?: string;
/** 링 색 (브랜드 등). 없으면 점수 구간별 자동 색 */
color?: string;
className?: string;
scoreClassName?: string;
};
function scoreStrokeColor(score: number, maxScore: number): string {
const pct = (score / maxScore) * 100;
if (pct <= 40) return "#D4889A";
if (pct <= 60) return "#7A84D4";
if (pct <= 80) return "#9B8AD4";
return "#6C5CE7";
}
export function ScoreRing({
score,
maxScore = 100,
size = 120,
label,
color,
className = "",
scoreClassName,
}: ScoreRingProps) {
const strokeWidth = size <= 72 ? 5 : 8;
const radius = (size - strokeWidth) / 2;
const circumference = 2 * Math.PI * radius;
const progress = Math.min(score / maxScore, 1);
const targetOffset = circumference * (1 - progress);
const resolvedColor = color ?? scoreStrokeColor(score, maxScore);
const [dashOffset, setDashOffset] = useState(circumference);
const defaultScoreClass =
size <= 72 ? "text-sm font-bold text-navy-900" : "text-2xl font-bold text-navy-900";
useEffect(() => {
setDashOffset(circumference);
const id = requestAnimationFrame(() => setDashOffset(targetOffset));
return () => cancelAnimationFrame(id);
}, [circumference, targetOffset]);
return (
<div className={`flex flex-col items-center gap-2 ${className}`.trim()}>
<div className="relative" style={{ width: size, height: size }}>
<svg
width={size}
height={size}
viewBox={`0 0 ${size} ${size}`}
className="-rotate-90"
aria-hidden
>
<circle
cx={size / 2}
cy={size / 2}
r={radius}
fill="none"
className="stroke-neutral-20"
strokeWidth={strokeWidth}
/>
<circle
cx={size / 2}
cy={size / 2}
r={radius}
fill="none"
stroke={resolvedColor}
strokeWidth={strokeWidth}
strokeLinecap="round"
strokeDasharray={circumference}
strokeDashoffset={dashOffset}
style={{ transition: "stroke-dashoffset 1s ease-out" }}
/>
</svg>
<div className="absolute inset-0 flex items-center justify-center">
<span className={scoreClassName ?? defaultScoreClass}>{score}</span>
</div>
</div>
{label ? <span className="body-14 text-neutral-60 text-center">{label}</span> : null}
</div>
);
}