[리액트] 별점 컴포넌트

파쏭쏭계란빡 ㅣ 2024. 1. 24. 23:10

 

 

 

사이드 프로젝트에서 필요한 별점 컴포넌트를 구현했다.

따로 아이콘 등을 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%
    ); /* 절반만 채워진 별 모양 */
}
[리액트] 별점 컴포넌트