[스프링부트 게시판] 29. 추천 기능 구현

2022. 9. 1. 12:02·🚗 Backend Toy Project/스프링 부트 게시판
  • 게시글에 추천 버튼을 추가하여 다른 사람의 게시글을 추천할 수 있도록 구현하였습니다.
  • 다만 본인이 작성한 게시글의 경우에는 추천이 불가능하며 한 명의 사용자는 하나의 게시글에 한 번의 추천만 가능하도록 하였습니다.
  • 이후 해당 기능을 이용하여 조회수 및 추천수를 기준으로 정렬할 수 있는 기능을 추가해보고 싶었습니다.

📝 Recommend

  • 우선 추천 엔티티를 아래와 같이 구현해주었습니다.
package com.cos.blog.model;

...

@Entity
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
@Table(
    uniqueConstraints = {
        @UniqueConstraint(
            name = "recommend_uk",
            columnNames = {"board_id" , "user_id"} 
        )
    }
)
public class Recommend {

    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @JoinColumn(name = "board_id")
    @ManyToOne(fetch = FetchType.LAZY)
    private Board board;

    @JoinColumn(name = "user_id")
    @ManyToOne(fetch = FetchType.LAZY)
    private User user;
}
  • 한 명의 사용자가 하나의 게시글에 여러 번 추천을 할 수 없도록 하기 위해 @Table 어노테이션을 사용하여 제약조건을 부여해주었습니다.
  • Recommend 개체는 추천을 받은 게시글과 해당 게시글을 추천한 사용자에 대한 정보로 구성되어 있습니다.

📝 Board

  • Board 개체에는 아래와 같은 코드를 추가해주었습니다.
package com.cos.blog.model;

...

@Entity
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class Board {

    ...

    @OneToMany(mappedBy = "board", cascade = CascadeType.REMOVE)
    private List<Recommend> recommends;

    @Transient
    private boolean recommend_state;

    @Transient
    private int recommend_count;

    ...
}
  • recommends 변수에는 해당 게시글을 추천한 사용자에 대한 정보가 들어갈 것이며 recommend_state, recommend_count 변수에는 각각 추천 상태와 추천 개수가 저장될 것입니다.
  • 또한 @Transient 어노테이션을 사용하여 데이터베이스에는 해당 컬럼이 생성되지 않도록 하였습니다.

📝 RecommendRepository

  • 엔티티를 새로 생성하였으니 Repository도 생성해줍니다.
package com.cos.blog.repository;

...

public interface RecommendRepository extends JpaRepository<Recommend, Long>{

    @Modifying
    @Query(value = "INSERT INTO recommend(board_id, user_id) VALUES(:board_id, :principal_id)", nativeQuery = true)
    int recommend(Long board_id, Long principal_id);

    @Modifying
    @Query(value = "DELETE FROM recommend WHERE board_id = :board_id AND user_id = :principal_id", nativeQuery = true)
    int cancelRecommend(Long board_id, Long principal_id);
}
  • 각각의 쿼리는 추천과 추천 취소를 담당합니다.

📝 RecommendService

  • 다음으로 Service 단을 구현해줍니다.
package com.cos.blog.service;

...

@RequiredArgsConstructor
@Service
public class RecommendService {

    private final RecommendRepository recommendRepository;

    @Transactional
    public void recommend(Long board_id, Long principal_id) {
        recommendRepository.recommend(board_id, principal_id);
    }

    @Transactional
    public void cancelRecommend(Long board_id, Long principal_id) {
        recommendRepository.cancelRecommend(board_id, principal_id);
    }
}

📝 BoardApiController

  • Controller는 따로 생성해주지 않고 기존에 구현해놓았던 BoardApiController에 코드를 추가해주었습니다.
package com.cos.blog.controller.api;

@RestController
@RequiredArgsConstructor
public class BoardApiController {

    private final RecommendService recommendService;

    ...

    @PostMapping("/api/board/{board_id}/recommend")
    public ResponseDto<Integer> recommend(@PathVariable("board_id") Long board_id, @AuthenticationPrincipal PrincipalDetail principal) {
        recommendService.recommend(board_id, principal.getUser().getId());
        return new ResponseDto<Integer>(HttpStatus.CREATED.value(), 1);
    }

    @DeleteMapping("/api/board/{board_id}/recommend")
    public ResponseDto<Integer> cancelRecommend(@PathVariable("board_id") Long board_id, @AuthenticationPrincipal PrincipalDetail principal) {
        recommendService.cancelRecommend(board_id, principal.getUser().getId());
        return new ResponseDto<Integer>(HttpStatus.OK.value(), 1);
    }
}

📝 BoardService

  • 다음으로 게시글 페이지를 출력할 때 해당 사용자의 게시글 추천 상태에 따라 버튼이 알맞게 출력되도록 코드를 추가해주었습니다.
package com.cos.blog.service;

...

@Service
@RequiredArgsConstructor
public class BoardService {

    ...

    @Transactional
    public Board detail(Long id, HttpServletRequest request, HttpServletResponse response, Long principal_id) {

        ...

        Board board = boardRepository.findById(id).orElseThrow(() -> {
            return new IllegalArgumentException("글 상세보기 실패: 아이디를 찾을 수 없습니다.");
        });

        board.getRecommends().forEach((recommend) -> {
            if(recommend.getUser().getId() == principal_id) {
                board.setRecommend_state(true);
            }
        });
        board.setRecommend_count(board.getRecommends().size());

        return board;
    }

    ...

📝 detail.jsp, board.js

  • 마지막으로 View 단의 코드를 아래와 같이 수정 및 추가해주었습니다.

 

detail.jsp

<c:choose>
    <c:when test="${board.recommend_state}">
        <div style="text-align: center;">
            <c:choose>
                <c:when test="${board.user.id != principal.user.id}">
                    <button onClick="index.recommend(${board.id}, ${board.recommend_state})" class="btn btn-success" style="display: inline-block;">
                        추천 <span>${board.recommend_count}</span>
                    </button>
                </c:when>
                <c:otherwise>
                    <button onClick="index.recommend(${board.id}, ${board.recommend_state})" class="btn btn-success" style="display: inline-block;" disabled>
                        추천 <span>${board.recommend_count}</span>
                    </button>
                </c:otherwise>
            </c:choose>
        </div>
    </c:when>
    <c:otherwise>
        <div style="text-align: center;">
            <c:choose>
                <c:when test="${board.user.id != principal.user.id}">
                    <button onClick="index.recommend(${board.id}, ${board.recommend_state})" class="btn btn-outline-success" style="display: inline-block;">
                        추천 <span>${board.recommend_count}</span>
                    </button>
                </c:when>
                <c:otherwise>
                    <button onClick="index.recommend(${board.id}, ${board.recommend_state})" class="btn btn-outline-success" style="display: inline-block;" disabled>
                        추천 <span>${board.recommend_count}</span>
                    </button>
                </c:otherwise>
            </c:choose>
        </div>
    </c:otherwise>
</c:choose>

 

board.js

    ...

    recommend: function(board_id, recommend_state) {
        let recommend = $("#btn-recommend");
        if (!recommend_state) {
            $.ajax({
                type: "POST",
                url: `/api/board/${board_id}/recommend`,
                dataType: "json"
            }).done(resp => {
                recommend.removeClass("btn-outline-success");
                recommend.addClass("btn-success");

                location.reload();
            }).fail(error => {
                console.log(error);
            });
        } else {
            $.ajax({
                type: "DELETE",
                url: `/api/board/${board_id}/recommend`,
                dataType: "json"
            }).done(resp => {
                recommend.removeClass("btn-success");
                recommend.addClass("btn-outline-success");

                location.reload();
            }).fail(error => {
                console.log(error);
            });
        }
    }
}

📝 Result


💡 알게 된 점

  • 개체 생성 및 연관관계 설정, Repository, Service, Controller, View까지의 모든 작업을 스스로 구현해보며, 기능 구현의 전체적인 흐름을 파악

 

GitHub - Daegwon-Kim/SpringBoot-JPA-Blog

Contribute to Daegwon-Kim/SpringBoot-JPA-Blog development by creating an account on GitHub.

github.com

 

저작자표시 (새창열림)

'🚗 Backend Toy Project > 스프링 부트 게시판' 카테고리의 다른 글

[스프링부트 게시판] 31. 이전글, 다음글 구현  (0) 2022.09.06
[스프링부트 게시판] 30. 게시판 정렬 기능 구현  (0) 2022.09.03
[스프링부트 게시판] 28. 게시글 검색 기능 구현  (0) 2022.08.30
[스프링부트 게시판] 27. 게시글 작성일 및 조회수 추가  (0) 2022.07.03
[스프링부트 게시판] 26. 커스텀 Validation을 통한 중복 검사 구현  (0) 2022.07.02
'🚗 Backend Toy Project/스프링 부트 게시판' 카테고리의 다른 글
  • [스프링부트 게시판] 31. 이전글, 다음글 구현
  • [스프링부트 게시판] 30. 게시판 정렬 기능 구현
  • [스프링부트 게시판] 28. 게시글 검색 기능 구현
  • [스프링부트 게시판] 27. 게시글 작성일 및 조회수 추가
Baeg-won
Baeg-won
  • Baeg-won
    좋았다면 추억이고 나빴다면 경험이다.
    Baeg-won
  • 전체
    오늘
    어제
    • 분류 전체보기
      • 🍃 Spring, Spring Boot
        • 스프링 프레임워크 기초
        • 스프링 핵심 원리 - 기본편
        • 자바 ORM 표준 JPA 프로그래밍 - 기본편
        • 스프링 MVC
        • 실전! 스프링 부트와 JPA 활용1 - 웹 애플리..
      • 🥑 Web Technoloy
      • 🚗 Backend Toy Project
        • 스프링 부트 게시판
        • Photogram
        • Baeg-won Clothing Gallery
      • 🥇 Problem Solving
        • Breadth-First Search
        • Depth-First Search
        • Backtracking
        • Simulation
        • Two-pointer
        • Binary Search
        • Greedy
        • Dynamic Programming
        • Minimum Spanning Tree
        • Dijkstra
        • Floyd warshall
      • ☕ Java
        • 명품 자바 에센셜
        • Applications
      • 🍦 JavaScript
        • JavaScript 기초
      • 🐧 Linux
        • 이것이 리눅스다(CentOS 8)
      • 📟 Database
        • 혼자 공부하는 SQL
      • 🧬 Data Structure
      • 🎬 HTML
      • 🎤 Tech Interview
      • 📌 etc
        • Unity 2D Raising Jelly Game
        • C++
        • 영어 쉐도잉
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

  • 공지사항

  • 인기 글

  • 태그

  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.3
Baeg-won
[스프링부트 게시판] 29. 추천 기능 구현
상단으로

티스토리툴바