[스프링부트+JPA+타임리프] 게시물 좋아요 기능

jyleever·2022년 5월 30일
3

게시물 상세 조회할 때 로그인 유저가 작성한 게시물이 아닌 경우에만 좋아요 버튼이 활성화된다.
로그인 유저가 좋아요 하지 않은 게시물일 때 좋아요 를 누를 수 있도록 한다!
좋아요한 게시물이라면 좋아요를 취소할 수 있도록 한다.

  • 로그인 유저가 좋아요한 게시물이라면 빨간 하트 그림이 보이고 좋아요한 게시물이 아니라면 빈 하트 그림이 보인다.

PostController

  • 상세 조회 게시물 페이지를 반환할 때 현재 로그인한 유저가 좋아요한 게시물인지 아닌지를 파악해야 한다.
  • 리포지토리에서 데이터가 존재하는지 확인한 boolean 결괏값인 like 변수를 통해 파악한다.
        boolean like = false; // 비로그인 유저라면 무조건 like = false;

        if(user != null){
            // 로그인한 사용자라면

            /* member_id 반환 */
            Long member_id = user.getMember().getId();
			
            ...
            
            /* 현재 로그인한 유저가 이 게시물을 좋아요 했는지 안 했는지 여부 확인 */
            like = postService.findLike(post_id, member_id);
        }

        model.addAttribute("like", like);

PostService

    /** 글 좋아요 확인 **/
    @Override
    public boolean findLike(Long post_id, Long member_id) {

        return memberLikePostRepository.existsByPost_IdAndMember_Id(post_id, member_id);

    }

MemberLikePostRepository

    /** 유저가 특정 게시물을 좋아요 했는지 확인 **/
    boolean existsByPost_IdAndMember_Id(Long post_id, Long member_id);


https://docs.spring.io/spring-data/jpa/docs/current/reference/html/#jpa.query-methods.query-creation

  • 스프링 데이터 JPA는 메소드 이름을 분석해서 JPQL을 생성하고 실행
  • Post 엔티티의 Id와 Member 엔티티의 Id 이용해 MemberLikePost 데이터가 존재하는지 확인
  • 애플리케이션 로딩 시점에 오류를 인지할 수 있는 것이 스프링 데이터 JPA의 매우 큰 장점

뷰 화면에서 좋아요를 눌렀을 때 처리는 아래와 같다.

post-read.html

  • 로그인 유저와 작성자가 동일하지 않은 경우에만 좋아요 이미지를 볼 수 있다.
  • 만약 로그인 유저가 좋아요한 게시물이라면 속이 찬 하트 이미지를 보여주고, 이미지를 클릭하면 좋아요를 취소할 수 있도록 한다.
  • 로그인 유저가 좋아요하지 않은 게시물이라면 빈 하트를 보여주고 좋아요를 누를 수 있도록 한다.

  • 비로그인 유저가 좋아요를 클릭하면 로그인하라는 메시지 알림창이 나오도록 한다

  • 좋아요를 눌렀을 때 이벤트를 설정하기 위해 img 태그에 id="likeImg", id="loginCheck" 속성을 부여한다.
  • 타임리프 th:value 속성을 이용해 서버로부터 like boolean 변수를 받아와 유저의 좋아요 여부를 판단한다.
  • 스프링 시큐리티 사용 시 적용할 수 있는 타임리프 속성 sec:authorize-expr 을 이용하여 로그인 여부 판단
  • 타임리프 속성 th:unless="${#strings.equals(post.getMember_id(),login_id)}" 이용하여 현재 로그인 유저와 게시글 유저가 동일하지 않은지 판단
<div sec:authorize-expr="isAuthenticated()" or th:unless="${#strings.equals(post.getMember_id(),login_id)}" class="d-block">
    <!-- 로그인 유저와 작성자가 동일하지 않다면 -->
    <!-- 좋아요 -->
    <input type="hidden" id="like_check" th:value="${like}">
    <img th:id="likeImg" src="/assets/img/like_empty.png" alt="" width="30px"
    height="30px">

    <span th:text="${post.likeCount}"></span>
</div>
<div sec:authorize-expr="!isAuthenticated()">
    <!-- 로그인하지 않은 유저라면 -->
    <img id="loginCheck" src="/assets/img/like_empty.png" alt="" width="30px"
    height="30px">
    <span th:text="${post.likeCount}"></span>
</div>

post-read.html - ajax

<script>
  
    const clickLikeUrl = "/assets/img/like_click.png";
    const emptyLikeUrl = "/assets/img/like_empty.png";

    /** 좋아요 유무에 따라 하트 그림 다르게 보여줌 **/
    //브라우저가 웹 문서를 읽기 시작하고 DOM이 생성되면 실행되는 메소드
    $(function(){

        // 현재 로그인한 유저가 해당 게시물을 좋아요 했다면 likeVal = true, 
  		// 좋아요하지 않았다면 false
        let likeVal = $('#like_check').val(); // 데이터가 있으면 true
        const likeImg = $('#likeImg');

        console.log("likeVal : " + likeVal);

        if(likeVal === 'true'){
            // 데이터가 존재하면 화면에 채워진 하트 보여줌
            $('#likeImg').attr("src", clickLikeUrl);
        } else if(likeVal === 'false'){
            // 데이터가 없으면 화면에 빈 하트 보여줌
            $('#likeImg').attr("src", emptyLikeUrl);
        }
    });

    /** 좋아요 클릭 시 실행 **/
    $('#likeImg').click(function() {

        const postId = $('#postId').val();
        const likeVal = $('#like_check').val();

        console.log(likeVal);
        if (likeVal === 'true') {
            const con_check = confirm("현재 게시물 추천을 취소하시겠습니까?")
            if (con_check) {
                console.log("추천 취소 진입");
                $.ajax({
                    type: 'POST',
                    url: '/rest/community/post/like/' + postId,
                    contentType: 'application/json; charset=utf-8'
                }).done(function () {
                    $('#likeImg').attr("src", emptyLikeUrl);
                    location.reload();
                }).fail(function (error) {
                    alert(JSON.stringify(error));
                })
            }
        } else if(likeVal === 'false'){
            const con_check = confirm("현재 게시물을 추천하시겠습니까?");
            if (con_check) {
                console.log("추천 진입");
                $.ajax({
                    type: 'POST',
                    url: '/rest/community/post/like/' + postId,
                    contentType: 'application/json; charset=utf-8'
                }).done(function () {
                    $('#likeImg').attr("src", clickLikeUrl);
                    location.reload();
                }).fail(function (error) {
                    alert(JSON.stringify(error));
                })
            }
        }
    });

    /** 로그인하지 않은 유저가 좋아요 누를 때 **/
    $('#loginCheck').click(function(){
        alert("로그인 후 이용할 수 있습니다.");
    });

</script>
  • rest/community/like/{post_id} url에 post 메소드로 통신하여 좋아요 여부에 따라 좋아요 저장/취소

PostRestController

    /** 글 좋아요 **/
    @PostMapping("/like/{post_id}")
    public boolean like(@PathVariable Long post_id, @AuthenticationPrincipal UserAdapter user){

        Long member_id = user.getMemberDto().getId();
        // 저장 true, 삭제 false
        boolean result = postService.saveLike(post_id, member_id);
        return result;
    }
  • PostServicesaveLike 메서드 호출하여 좋아요 저장/취소(삭제)

PostService

    /** 글 좋아요 **/
    @Override
    public boolean saveLike(Long post_id, Long member_id) {

        /** 로그인한 유저가 해당 게시물을 좋아요 했는지 안 했는지 확인 **/
        if(!findLike(post_id, member_id)){

            /* 좋아요 하지 않은 게시물이면 좋아요 추가, true 반환 */
            Member member = memberRepository.findById(member_id).orElseThrow(() ->
                    new IllegalArgumentException("해당 회원이 존재하지 않습니다."));
            Post post = postRepository.findById(post_id).orElseThrow(() ->
                    new IllegalArgumentException("해당 게시물이 존재하지 않습니다."));

            /* 좋아요 엔티티 생성 */
            MemberLikePost memberLikePost = new MemberLikePost(member, post);
            memberLikePostRepository.save(memberLikePost);
            postRepository.plusLike(post_id);

            return true;
        } else {

            /* 좋아요 한 게시물이면 좋아요 삭제, false 반환 */
            memberLikePostRepository.deleteByPost_IdAndMember_Id(post_id, member_id);
            postRepository.minusLike(post_id);

            return false;
        }
    }

글 좋아요

  • new 연산자를 이용해 MemberLikePost 객체를 생성하여 리포지토리에 저장
  • 해당 post에 plusLike 메서드 호출하여 좋아요 값 + 1

글 좋아요 취소

  • MemberLikePostRepository에서 deleteByPost_IdAndMember_Id 메서드를 호출하여 해당 MemberLikePost 객체 삭제
  • 해당 post에 minusLike 메서드 호출하여 좋아요 값 - 1

PostRepository

    /** 좋아요 추가 **/
    @Modifying
    @Query(value = "update Post post set post.likeCount = post.likeCount + 1 where post.id = :post_id")
    int plusLike(@Param("post_id") Long post_id);
    
    /** 좋아요 삭제 **/
    @Modifying
    @Query(value = "update Post post set post.likeCount = post.likeCount - 1 where post.id = :post_id")
    int minusLike(@Param("post_id") Long post_id);

@Query

  • 리파지토리 인터페이스에 쿼리 직접 정의
  • 이름없는 Named 쿼리라 할 수 있음
  • JPA Named 쿼리처럼 애플리케이션 실행 시점에 문법 오류를 발견할 수 있음(매우 큰 장점!)
  • 참고: 실무에서는 메소드 이름으로 쿼리 생성 기능은 파라미터가 증가하면 메서드 이름이 매우 지저분해진다. 따라서 @Query 기능을 자주 사용하게 된다.

@Modifying

  • @Query 로 작성된 삽입, 변경, 삭제 쿼리 메서드 사용할 때 필요
  • 조회 쿼리를 제외, 데이터에 변경이 일어나는 INSERT, UPDATE, DELETE, DDL 에서 사용
  • 스프링 jpa 에서 @Modifying 옵션을 사용하면 query가 나가고 난 후 clear 과정을 자동으로 해준다. 따라서 따로 flush clear 하지 않아도 됨

0개의 댓글