88 lines
2.5 KiB
TypeScript
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>
|
|
);
|
|
}
|