Day8 - 게시물 투표 & 커뮤니티 완성

RINM·2023년 1월 10일

NextJS - Reddit Clone

목록 보기
8/9
post-thumbnail

Vote

vote

Frontend

comment 일때와 post 일때를 구분하여 처리
이미 누른 경우 투표 무효 처리

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

        //if aleady clicked, reset the vote
        if((!comment&&value===post?.userVote)||(comment && comment.userVote ===value)) value=0;

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

backend

post일때, comment 일때 나누어서 처리
vote 생성 혹은 수정시 post 정보를 다시 가져와 vote 다시 지정

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

    //value check
    if(![-1,0,1].includes(value)) return res.status(400).json({value:"Only -1, 0, 1 value is possible"})

    try {
        const user:User = res.locals.user;
        let post: Post | undefined = await Post.findOneByOrFail({identifier,slug});
        let vote: Vote | undefined;
        let comment: Comment | undefined;

        //find vote on DB
        if(commentIdentifier){//comment vote
            comment= await Comment.findOneByOrFail({identifier:commentIdentifier});
            vote = await Vote.findOneBy({username:user.username,commentId: comment.id});
        } else{ //post vote
            vote = await Vote.findOneBy({username:user.username,postId: post.id});
        }

        //Vote not found
        if(!vote && value===0) return res.status(404).json({error:"Vote not found"})
        else if(!vote){ //create new vote
            vote = new Vote();
            vote.user=user;
            vote.value=value;

            if(comment) vote.comment = comment; //comment vote
            else vote.post = post; //post vote

            await vote.save()
        }else if(value===0){ //reset vote
            await vote.remove();
        }else if(vote.value!==value){//update vote
            vote.value=value;
            await vote.save();
        } 

        //find post 
        post = await Post.findOneOrFail({
            where:{identifier, slug},
            relations : ["comments","comments.votes","sub","votes"]
        }
        )

        //set vote info to post
        post.setUserVote(user);
        post.comments.forEach((c)=>c.setUserVote(user))

        return res.json(post);
    } catch (error) {
        console.error(error);
        return res.status(500).json({error:"Something went wrong"})
    }
}

router.post("/",userMiddleware,authMiddleware,vote)

커뮤니티 메인 포스트 리스트

subpostlist

PostCard


커뮤니티에 소속된 post를 각각 랜더링하는 컴포넌트
post 페이지와 비슷한 UI로 구성
vote 함수는 따로 작성한다.

    const vote = async (value:number) => {
        if(!authenticated) router.push('/login')
        
        if(value === userVote) value =0;

        try {
            await axios.post('/votes',{
                identifier,
                slug,
                value
            })
            if(subMutate){
                subMutate();
            }
        } catch (error) {
            console.error(error);
        }
    }

메인페이지 포스트 목록 완성

main

Frontend

useSWRInfinite

페이지 매김 및 무한 로딩을 위한 SWR API

const getKey = (pageIndex, previousPageData) => {
  if (previousPageData && !previousPageData.length) return null
  return `/users?page=${pageIndex}&limit=10`                    
}

const { data, size, setSize } = useSWRInfinite(getKey, fetcher)

더이상 요청할 데이터가 없을 때까지 로딩. 로딩할 데이터가 없으면 null 반환

  • data: 가져오기 응답 배열
  • size: 가져올 페이지/반환될 페이지 수
  • setSize: 가져와야하는 페이지 수 설정
  const getKey = (pageIndex: number, previousPageData:Post[]) =>{
    if(previousPageData && !previousPageData.length) return null;
    return `/posts?page=${pageIndex}`;
  }
  const {data, error, size:page, setSize:setPage, isValidating, mutate}=useSWRInfinite<Post[]>(getKey)

Intersection observer

스크롤을 내릴 때 포스트를 더 가져오기 위하여 사용
마지막 element를 관찰하다가 스크롤을 내리면서 viewport에서 intersection이 발생하면 포스트를 추가로 로딩

const [observedPost, setobservedPost] = useState("")

  useEffect(() => {
    if(!posts || posts.length===0) return;

    const id = posts[posts.length-1].identifier;
    if(id!==observedPost){
      setobservedPost(id);
      observedElement(document.getElementById(id))
    }
  
  }, [posts])

const observedElement = (element: HTMLElement | null) =>{
    if(!element) return;

    const observer = new IntersectionObserver(
      (entries) =>{
        //reached bottom
        if(entries[0].isIntersecting === true){
          // console.log("reached bottom");
          
          setPage(page+1);
          observer.unobserve(element);
        }
      },
      {threshold:1}
    );

    observer.observe(element);
  }

Backend

정렬된 목록에서 이미 가져온 개수 만큼 지나친 뒤(skip) perPage 만큼 가져오기

const getPosts =async (req: Request, res: Response) => {
    const currentPage: number = (req.query.page || 0 ) as number;
    const perPage: number = (req.query.count || 8) as number;

    try {
        const posts = await Post.find({
            order:{createdAt:"DESC"},
            relations: ["sub","votes","comments"],
            skip: currentPage*perPage,
            take: perPage,
        })

        if(res.locals.user){
            posts.forEach((p)=> p.setUserVote(res.locals.user));
        }

        return res.json(posts);
    } catch (error) {
        console.error(error);
        return res.status(500).json({error:"Something went wrong"})
    }
}

0개의 댓글