#typescript #next-js #tailwindcss

포스트와 댓글을 위한 Vote (좋아요 싫어요) 기능 구현하기

Nov 20, 2022


포스트, 코멘트 투표 UI

포스트와 코멘트 투표 완성 UI는 아래와 같다


포스트, 코멘트 투표 UI 템플릿 작성

포스트 내용 페이지 내 포스트, 코멘트 투표 UI를 추가해야 하므로
reddit-clone-app\client\src\pages\r\[sub]\[identifier]\[slug].tsx 파일에 아래 내용을 추가한다.

[포스트 투표 부분]
...생략
{post && (
    <>
        <div className="flex">
            {/* 투표 가능 부분 */}
            <div className="flex-shrink-0 w-10 py-2 text-center rounded-l">
                {/* 좋아요 부분 */}
                <div
                    className="w-6 mx-auto text-gray-400 rounded cursor-pointer hover:bg-gray-300 hover:text-red-500"
                    // onClick={() => vote(1)}
                >
                    <i
                        className={classNames("fas fa-arrow-up", {
                            "text-red-500": post.userVote === 1
                        })}
                    >
                    </i>
                </div>
                <p className="text-xs font-bold">{post.voteScore}</p>
                {/* 싫어요 부분 */}
                <div
                    className="w-6 mx-auto text-gray-400 rounded cursor-pointer hover:bg-gray-300 hover:text-blue-500"
                    // onClick={() => vote(-1)}
                >
                    <i
                        className={classNames("fas fa-arrow-down", {
                            "text-blue-500": post.userVote === -1
                        })}
                    >
                    </i>
                </div>
            </div>
...생략
[코멘트 투표 부분]
...생략
{comments?.map((comment) => (
    <div className="flex" key={comment.identifier}> 
        {/* 투표 기능 부분 */}
        <div className="flex-shrink-0 w-10 py-2 text-center rounded-l">
            {/* 좋아요 부분 */}
            <div
                className="w-6 mx-auto text-gray-400 rounded cursor-pointer hover:bg-gray-300 hover:text-red-500"
                // onClick={() => vote(1, comment)}
            >
                <i
                    className={classNames("fas fa-arrow-up", {
                        "text-red-500": comment.userVote === 1
                    })}
                >
                </i>
            </div>
            <p className="text-xs font-bold">{comment.voteScore}</p>
            {/* 싫어요 부분 */}
            <div
                className="w-6 mx-auto text-gray-400 rounded cursor-pointer hover:bg-gray-300 hover:text-blue-500"
                // onClick={() => vote(-1, comment)}
            >
                <i
                    className={classNames("fas fa-arrow-down", {
                        "text-blue-500": comment.userVote === -1
                    })}
                >
                </i>
            </div>
        </div>
...생략

vote 함수 생성

투표 UI에서 좋아요, 싫어요 투표 시 호출하는 vote함수를 추가한다.

    const vote = async (value: number, comment?:Comment) => {
        if(!authenticated) router.push("/login");

        // 이미 클릭한 vote 버튼을 눌렀을 시에는 자신이 한 투표를 reset하기 위해 value=0
        if(
            (!comment && value === post?.userVote) || 
            (comment && comment.userVote === value)
        ) {
            value = 0;
        }

        try {
            await axios.post("/votes", {
                identifier,
                slug,
                commentIdentifier: comment?.identifier,
                value
            })
        } catch (error) {
            console.log(error);
        }
    }

votes API 생성

reddit-clone-app\server\src\routes\votes.ts 라우트 파일을 생성하고 아래와 같이 내용을 추가한다.

import { Router } from "express";
import userMiddleware from "../middlewares/user";
import authMiddleware from "../middlewares/auth";

const vote = () => {
    
}

const router = Router();
router.post("/", userMiddleware, authMiddleware, vote);

export default router;

그리고 서버가 votes 요청을 받을 수 있도록 reddit-clone-app\server\src\server.ts 엔트리 파일에 라우트를 연결한다.
아래 내용을 추가한다.

import voteRoutes from "./routes/votes";
...생략
// "/api/votes/..." 요청 시 해당 라우터로 연결
app.use("/api/votes", voteRoutes)
...생략

vote 핸들러 작성

POST /api/votes 요청 시 실행되는 vote 함수를 아래와 같이 정의한다.

...생략
const vote = async (req: Request, res: Response) => {
    const { identifier, slug, commentIdentifier, value } = req.body;

    // -1, 0, 1의 value만 오는지 체크 
    if(![-1, 0 ,1].includes(value)) {
        return res.status(400).json({ value: "-1, 0, 1의 value만 올 수 있습니다." });
    }

    try {
        // userMiddleware 에서 가져온 user 정보 초기화
        const user: User = res.locals.user;
        // 현재 포스트 정보를 통해 Post 데이터 가져오기
        let post: Post = await Post.findOneByOrFail({ identifier, slug });
        // vote 초기화
        let vote: Vote | undefined;
        // comment 초기화
        let comment: Comment;

            // vote 객체 찾기
            if(commentIdentifier) {
                // 댓글 식별자가 있는 경우 댓글로 vote 찾기
                comment = await Comment.findOneByOrFail({ identifier: commentIdentifier });
                vote = await Vote.findOneBy({ username: user.username, commentId: comment.id });
            } else {
                // 댓글 식별자가 없으면 포스트로 vote 찾기
                vote = await Vote.findOneBy({ username: user.username, postId: post.id });
            }

            if(!vote && value === 0) {
                // vote이 없고 value가 0인 경우 오류 반환
                // value = 0 : 좋아요 또는 싫어요를 중복으로 누른 경우 = vote 데이터가 존재
                return res.status(400).json({ error: "Vote을 찾을 수 없습니다." });
            } else if(!vote) {
                // 첫 투표인 경우
                vote = new Vote();
                vote.user = user;
                vote.value = value;

                // 게시물에 속한 vote or 댓글에 속한 vote
                if(comment) vote.comment = comment
                else vote.post = post;
                await vote.save();
            } else if (vote.value !== value) {
                // 조회된 vote 값과 요청 value가 다르면 
                //  => 첫 투표와 다른 투표 값 입력
                vote.value = value;
                await vote.save();
            }

            // 포스트 데이터 관련 테이블 조인하여 조회
            post = await Post.findOneOrFail({
                where: {
                    identifier, slug
                },
                relations: ["comments", "comments.votes", "sub", "votes"]
            })

            // 현 사용자의 포스트 투표 반영
            post.setUserVote(user);
            // 현 사용자의 코멘트 투표 반영 (댓글마다 loop)
            post.comments.forEach(c => c.setUserVote(user));

            return res.json(post);
    } catch (error) {
        console.log(error);
        return res.status(500).json({ error: "문제가 발생했습니다." });
    }
}
...생략


*****

© 2021, Ritij Jain | Pudhina Fresh theme for Jekyll.