컴퓨터/웹 : JS

[Node.js](개인 프로젝트) 쇼페이지 완성, 별점 표시

도도새 도 2023. 2. 27. 08:19

쇼페이지

 

오늘은 axios통신을 모두 끝내고 show페이지의 요약 후기 메뉴탭을 완성시키려고한다.

 

우선 점수의 평균을 구해서 가져와야하는데, 이미 모델에서 가상 속성으로 추가해 놓았기 때문에 가져다 쓰면 된다.

 

cafeSchema .virtual ('ratingAVG').get (function () {

  const comments = this .comment || [];

  const numComments = comments .length ;

  const ratings = comments .map (comment =>comment .rating ? comment .rating :{ taste :0 , atmosphere :0 , price :0 });

  const sum = {

   taste :ratings .reduce ((total , rating ) =>total + rating .taste , 0 ),//total = 누적값, rating = 배열의 현재요소

   atmosphere :ratings .reduce ((total , rating ) =>total + rating .atmosphere , 0 ),

   price :ratings .reduce ((total , rating ) =>total + rating .price , 0 )

  };

  console .log ('가상', sum );

  return {

   taste :numComments ? sum .taste / numComments : 0 ,

   atmosphere :numComments ? sum .atmosphere / numComments : 0 ,

   price :numComments ? sum .price / numComments : 0

  };

});

 

가상속성 사용

app.js

app .get ('/cafe/api/totalRating/:id', async (req , res )=>{

  const cafe = await Cafe .findById (req .params .id );

  const ratings = cafe .ratingAVG ;

  res .json (ratings );

})

이처럼 ratingAVG속성을 부르면 모델에서 정의한 위 계산들이 진행된 결과(return 값)이 반환된다. 이를 json으로 넘기도록 작성하였다.

 

App.js

  useEffect (()=>{

   const fetchData = async ()=>{

    try {

     const response = await axios .get (`http://localhost:8080/cafe/api/totalRating/${id }`);

     setRatings (response .data );

    }

    catch (e ){

     console .log (e );

    }

   }

   fetchData ();

  }, [id ]);

 그리고 useEffect 내에서 해당 값을 받아온다. 그런데 useEffect의 첫 번쨰 콜백 함수로 async를 쓰려 했더니 오류가 났다. useEffect에서 첫 번째 인자로는 동기 함수가 와야 한다고 한다. , 동기 함수를 우선 호출 한 후, 그 내에서 비동기 함수를 정의한 다음 사용하도록 하였다. await를 굳이 쓴 이유는 data를 실제로 사용하기 전 해당 data예 값이 존재한다는 것을 보장받고 싶어서였다. 물론 이럼에도 null/undefinde 체크를 하고 사용하려고 한다.

 

프로그레스 바를 쓰려 했는데 레이아웃을 적용했더니 좌우에 자동으로 패딩이 들어가는 문제가 있었다. padding 이너 스타일을 0px로 해서 해결하였다.

 

뷰는 이렇게 구성되었다.

 

    <Container className ="row mt-3 pt-3 pb-3 mb-3 text-center"style ={{border :`2px solid ${grayColor }`, minHeight :'5rem', width :'30rem' }}>{/*2번항목*/}

     <div className ="col-3">

      <span >평점 </span >

     </div >

     <div className ="col-9">

      <div className ="row"style ={{textAlign :'left'}}>

       <span className ="offset-1 col-3 mb-3"> </span >

        {ratings && <ProgressBar rate ={ratings .taste ?ratings .taste :0 }></ProgressBar >}

      </div >

      <div className ="row"style ={{textAlign :'left'}}>

       <span className ="offset-1 col-3 mb-3">분위기 </span >

       {ratings &&<ProgressBar rate ={ratings .atmosphere ?ratings .atmosphere :0 }></ProgressBar >}

      </div >

      <div className ="row"style ={{textAlign :'left'}}>

       <span className ="offset-1 col-3 mb-3">가격 </span >

       {ratings &&<ProgressBar rate ={ratings .price ?ratings .price :0 }></ProgressBar >}

      </div >

     </div >

    </Container >

axios로 받은 값을 rating으로 초기화 해서 사용했는데, 삼항 연산자를 이용해서 만약 값이 null이나 undefinde같은 경우 0으로 되게 ProgressBarprobs로 넘겼다.

 

게다가 ratings이라는 객체자체가 비었을 경우에는 아예 컴포넌트를 처리하지 않도록 해버렸다(ratings &&) 이렇게 하면 발생 가능한 문제는....다 해결 된 거겠지?

 

컴포넌트는 아래와 같다.

 

  function ProgressBar (probs ){

   const value = (probs .rate ).toFixed (2 );

   const percentage_num = (probs .rate /5 *100 ).toFixed (2 );

   const percentage =`${percentage_num }%`;

   return (

    <div className ="progress w-60 col-8"role ="progressbar"aria-valuemax ="5"style ={{padding :'0px'}}>

    <div className ="progress-bar"style ={{width :percentage }}>{value }</div >

    </div >

   )

  }

 

여기서 문제는 댓글을 생성해도 페이지가 새로 랜더링 되지 않는다는 것이다. 그말인 즉슨 UseEffect가 재사용이 안 된다는 것이고, 새로운 데이터를 받아와서 페이지에 보여주는 것이 안 된다는 것이다. , 댓글을 달아 평점이 바뀜에도 총 평균 평점은 새로고침 등의 작업을 해야 새로 바뀌게 된다.

 

부모에서 isCommentUpdate라는 스테이트를 이용해서, 댓글을 다는 곳에 set함수를 보낸 뒤 해당 값을 true로 바꾸고, 이 값을 다시 리뷰 점수를 관리하는 영역으로 probs으로 전달한 후, 그 값을 UseEffect의 의존성 배열에 추가하면 되지 않을까.

 

 

  const handleSubmit = (event ) => {

   event .preventDefault ();

   probs .setCommentUpdated (true );//추가

   submitForm ({

    comment :formState ,

    userID :userID ,

    tasteRate :tasteRate ,

    atmosRate :atmosRate ,

    priceRate :priceRate

   });

   probs .setParentData ({...{comment :formState } });

   handleCloseModal ();

  };

formsubmit을 핸들하는 함수에 저것을 추가하였다.

 

    <PageTwoSectionOne  isCommentUpdated = {isCommentUpdated }/>

 

 

  useEffect (()=>{

   const fetchData = async ()=>{

    try {

     const response = await axios .get (`http://localhost:8080/cafe/api/totalRating/${id }`);

     setRatings (response .data );

    }

    catch (e ){

     console .log (e );

    }

   }

   fetchData ();

  }, [id , probs .isCommentUpdated ]);

의존성 배열에 해당 값을 넘겨주었다. 그런데 false -> true로 바뀌거나 true->false로 바뀔 때만 해당 값이 바뀌기 때문에 랜더링이 새로 되는 것이었다.

 

   probs .setCommentUpdated ({...{state :true }});

 

핸들 부분을 이렇게 바꿔주었다. 이제 생성시 잘 작동한다. 삭제 시에도 변하도록 해줘야한다. 삭제 핸들 함수도 같은 식으로 처리해 주었다.

 

 

*...{}는 객체 전개 연산자(Object Spread Operator)이다. 객체 전개 연산자는 객체 안에 있는 모든 프로퍼티를 복사하여 새로운 객체를 만든다. (내부 값은 모두 같으나 다른 객체로 인식된다. 이를 얕은 복사라고 함..._id값은 물론 주소도 다른 공간에 할당됨)

 

+)문득 부모가 랜더링 되면 자식도 랜더링 되기에

    <PageTwoSectionOne  isCommentUpdated = {isCommentUpdated }/>

이 부분에 probs를 넘겨 줄 필요 있나, 하는 생각이 들었지만, 부모에게 probs을 받은 자식만 다시 랜더링 된다는 것을 떠올려냈다.

 

이제 원하는 대로 다 잘 되게 되었다.

 

별점 넣기

 

이번에는 댓글에도 별점의 평균을 보여주는 코드를 작성하고 싶다.

다행히 이전에 받아온 api내부에 원하는 데이터가 다 들어있다.

 

적당한 것을 찾았다. 사용 설명서를 조금 읽고 적용해보자.

https://www.npmjs.com/package/css-star-rating

react용으로 새로 찾았다.

https://www.npmjs.com/package/react-simple-star-rating

 

새로 파일을 하나 만들고 시키는대로 붙여넣어준다.

 

import React , { useState } from 'react'

import { Rating } from 'react-simple-star-rating'

export function MyComponent () {

  const [rating , setRating ] = useState (0 )

  // Catch Rating value

  const handleRating = (rate ) => {

   setRating (rate )

   // other logic

  }

  // Optinal callback functions

  const onPointerEnter = () =>console .log ('Enter')

  const onPointerLeave = () =>console .log ('Leave')

  const onPointerMove = (value , index ) =>console .log (value , index )

  return (

   <div className ='App'>

    <Rating

     onClick ={handleRating }

     onPointerEnter ={onPointerEnter }

     onPointerLeave ={onPointerLeave }

     onPointerMove ={onPointerMove }

     /* Available Props */

    />

   </div >

  )

}

이제 원하는 곳에서 import해서 사용할 수 있게 되리라.

이 별점에서 prob을 만지면서 prob에 대해서 정리해야겠다고 마음먹게 되었다. 오늘은 개발 후 prob에 대해서 정리해야겠다.

 

아무튼 아래처럼 임포트하여 컴포넌트로 사용 할 수 있게 되었다. 문서를 참조해 readOnly로 만들었다.

import {StarRating } from './starRating';

 

starRating 내부는 아래처럼 바꿔버렸다.

import React , { useState } from 'react'

import { Rating } from 'react-simple-star-rating'

export function StarRating (probs ) {

  return (

   <div className ='App'>

    <Rating

     initialValue ={probs .value }

     size ={15 }

     readonly

     /* Available Props */

    />

   </div >

  )

}

 

 

이렇게 적용하였다. 내부 리뷰 내용도 가운데 정렬이 아닌 왼쪽 정렬로 바꾸는 편이 깔끔해 보인다.

 

 

, 그리고 이상하게 부트스트랩이 안 먹히는 이슈가 있다. 얘를들어 아래와 같은 것들이 먹히지 않는다.

 

       <div className ='text-left'>{comment .content }</div >

부트스트랩 버전이 업그레이드 되면서 아래처럼 써야한다고 한다.

       <div className ='text-start'>{comment .content }</div >

혼란하다.

 

어쨌든 왼쪽 정렬 하고 전체 총별점을 띄운 후 이 페이지 구성을 끝내버리자.

 

  const totalRatingAvg = ratings ?(((ratings .taste + ratings .atmosphere + ratings .price )/3 ).toFixed (2 )):0 ;

총 별점은 이미 가져와진 데이터로 쉽게 구성하였다.

 

결론

 

결론적으로 이런 결과물이 나왔다. 만들고 보니 아래와 같은 사항들을 추가, 수정해야겠다.

프로필 사진을 추가하자

리뷰에 올린 사진을 볼 수 있는 영역(collapsearcodian이라고 부른다고 한다) 추가 displaynoneblock으로 왔다갔다하면 쉽게 가능한 부분이다.

 

+ 구성이 잘못되었다고 느끼는 것이 부모 요소에서 axios를 한 번만 받고 자식에서는 set만 해도 충분했을 것이라고 느낀다. 코드의 중복이 다수 발생한 것 같다.

 

남은 페이지들을 부모에서 한 번만 axios를 받아와 자식들에게 probs로 전달하도록 하겠다.

 

요약페이지도 비슷한 방식으로 간략하게 구현하였다. 원래 요약 페이지는 아래와 같이 구성하도록 기획했었다.

그런데 게시글도 아닌 카페의 리뷰에 베스트가 있다는 것이 이상하다. 또한 해당 사항을 구성하려면 리뷰에 좋아요를 해야하는데, 이는 모델 스키마를 다시 수정해야함을 의미한다.

 

물론 필요하다면 수정하는게 당연하겠지만, ‘베스트 리뷰라는 것이 참 이상하다. 최근 리뷰로 하는 건 어떨까?...라는 생각을 하다가 당연하게 해당 부분에는 카페의 설명이 들어가야 한다고 느꼈다.

 

이제 carousel크기를 좀 줄이고 실제 사진을 넣도록 API를 구성하면 될 것이다. 그리고 지도 역시 API를 이용해서 받아올 것이다. 이 두 작업이 끝나면 백엔드 쪽에서는 대체로 작업이 끝났다고 봐도 될 것같다. 오늘은 피곤하므로 여기까지.