- 게시글에 추천 버튼을 추가하여 다른 사람의 게시글을 추천할 수 있도록 구현하였습니다.
- 다만 본인이 작성한 게시글의 경우에는 추천이 불가능하며 한 명의 사용자는 하나의 게시글에 한 번의 추천만 가능하도록 하였습니다.
- 이후 해당 기능을 이용하여 조회수 및 추천수를 기준으로 정렬할 수 있는 기능을 추가해보고 싶었습니다.
📝 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까지의 모든 작업을 스스로 구현해보며, 기능 구현의 전체적인 흐름을 파악
'🚗 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 |