사이드 프로젝트에서 필요한 별점 컴포넌트를 구현했다.
따로 아이콘 등을 import 하고싶지 않아서 svg를 직접 넣는 방식으로 만들었다.
0.5점 단위로 별점을 줄수있고, 클릭과 드래그를 이용해서 점수를 선택할 수 있다.
별점 컴포넌트의 너비에 따라서 인덱스를 구하고, 그 컴포넌트에서도 반절을 나눠서 계산하는 방법을 이용했다.
//App.tsx
import React from "react";
import "./App.css";
import StarRating from "./components/star/StarRating";
function App() {
return (
<div className="App">
<StarRating />
</div>
);
}
export default App;
//StarRating.tsx
import { useRef, useState } from "react";
import Star from "./Star";
import "./StarRating.css";
const StarRating = () => {
const [rating, setRating] = useState<number>(0);
const [isDragging, setIsDragging] = useState<boolean>(false);
const starContainerRef = useRef<HTMLDivElement>(null);
const starWrapperSize = 400;
const startCount = 5;
const starWidth = starWrapperSize / startCount; // 한 별의 너비
const calculateRating = (e: React.MouseEvent<HTMLDivElement>): number => {
const { left } = starContainerRef.current.getBoundingClientRect();
const starIndex = Math.floor((e.pageX - left) / starWidth); //별 인덱스
const starOffset = (e.pageX - left) % starWidth; //별 왼쪽 끝부터 별의 오른쪽 마진끝까지 위치
const isOverHalf = starOffset > starWidth / 2; //반절보다 오른쪽 선택했는가
if (!starIndex && starOffset < starWidth / 4) {
return 0;
}
return starIndex + (isOverHalf ? 1 : 0.5);
};
const handleMouseMove = (e: React.MouseEvent<HTMLDivElement>) => {
if (e.buttons === 0) {
setIsDragging(false);
return;
}
if (isDragging) {
const newRating = calculateRating(e);
setRating(newRating);
}
};
const handleMouseDown = (e: React.MouseEvent<HTMLDivElement>) => {
setIsDragging(true);
const newRating = calculateRating(e);
setRating(newRating);
};
const handleMouseUp = () => {
setIsDragging(false);
};
return (
<div
ref={starContainerRef}
className="star-container"
style={{ width: `${starWrapperSize}px` }}
onMouseMove={handleMouseMove}
onMouseDown={handleMouseDown}
onMouseUp={handleMouseUp}
>
{[...Array(startCount)].map((_, i) => {
const isCheck = rating > i;
const isHalf = rating - i < 1;
const level = isCheck ? (isHalf ? 1 : 2) : 0;
console.log(`${i} > ${isHalf}`);
return <Star key={i} level={level} />;
})}
</div>
);
};
export default StarRating;
//Star.tsx
interface IStarProps {
level: 0 | 1 | 2;
}
const LEVEL = ["", "star-half-filled", "star-filled"];
const Star = ({ level = 0 }: IStarProps) => {
return (
<svg
width="80"
height="200"
viewBox="0 0 25 23"
className={`star ${LEVEL[level]}`}
>
<polygon
className="star-full"
points="9.9,0.4 12.5,7.6 20.1,7.6 13.8,12.6 16.5,19.7 9.9,14.7 3.3,19.7 6,12.6 -0.1,7.6 7.5,7.6 "
/>
{level === 1 && (
<polygon
className="star-half"
points="9.9,0.4 12.5,7.6 20.1,7.6 13.8,12.6 16.5,19.7 9.9,14.7 3.3,19.7 6,12.6 -0.1,7.6 7.5,7.6 "
/>
)}
</svg>
);
};
export default Star;
//StarRating.css
.star-container {
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
}
.star {
fill: none;
transition: fill 200ms;
}
.star-full {
fill: gray;
}
.star-filled .star-full {
fill: gold;
}
.star-half-filled .star-half {
fill: gold;
clip-path: polygon(
0 0,
50% 0,
50% 100%,
0% 100%
); /* 절반만 채워진 별 모양 */
}
'리액트' 카테고리의 다른 글
useLayoutEffect 훅이란? (1) | 2024.02.05 |
---|---|
리액트 useImperativeHandle, forwardRef로 부모 컴포넌트가 자식 컴포넌트의 메서드나 변수에 직접 접근 (0) | 2024.01.17 |
리액트에서 dangerouslySetInnerHTML 렌더링하기 (0) | 2023.10.11 |
리액트에서 string 데이터와 ReactElement가 혼합된 데이터 렌더링 하기 (0) | 2023.10.11 |
FileReader로 파일 읽고 업로드한 파일 미리보기 구현 (0) | 2023.09.18 |
[리액트] 별점 컴포넌트