[Spring Boot] 40. 스프링부트 블로그 v2 (JPA) (10) 게시글 좋아요 1 - isLove & loveCount

김주희's avatar
Apr 07, 2025
[Spring Boot] 40. 스프링부트 블로그 v2 (JPA) (10) 게시글 좋아요 1 - isLove & loveCount

1. 비지니스 파악

1. user - 좋아요 - board

user -> board (좋아요) 1:N (유저는 여러 게시글을 좋아할 수 있음) N:1 (하나의 게시글은 여러 유저에게 좋아요 받을 수 있음) => N:M 관계이므로 동사 형태의 중간 테이블(love_tb)이 필요 -> love_tb(id, user_id(FK), brd_id(FK)) user - love - board 1 : N | N : 1 (N에 FK) OneToMany | ManyToOne -> 전부 ManyToOne으로 처리

2. 제약

  • 한 유저가 같은 게시글에 여러 번 좋아요를 누르지 못하게 제약 조건 설정
    • user_id와 board_id를 묶어 복합키 유니크 약 설정
  • JPA의 @ManyToMany는 제약이 많기 때문에 사용하지 않음
    • 랜덤 테이블명 등
 
게시글 1번으로 이동, 나는 ssar (pk는 화면에 안보여도 들고가야 update 등 써야하는 곳이 있음) -> id title content username is_owner is_love love_count -> 1 제목1 내용1 ssar true true 2 user - love - board 1 : N N : 1 (N에 FK) OneToMany ManyToOne -> 그냥 ManyToOne으로 하세용
 

2. love_tb

1. Love Table (Entity) 설계

package shop.mtcoding.blog.love; import jakarta.persistence.*; import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; import org.hibernate.annotations.CreationTimestamp; import shop.mtcoding.blog.board.Board; import shop.mtcoding.blog.user.User; import java.sql.Timestamp; @NoArgsConstructor @Getter @Table( name = "love_tb", uniqueConstraints = { @UniqueConstraint(columnNames = {"user_id", "board_id"}) } ) @Entity public class Love { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Integer id; @ManyToOne(fetch = FetchType.LAZY) // 항상 가지고 와야하지만(EAGER) id만으로도 조회가능하니까(LAZY) private User user; @ManyToOne(fetch = FetchType.LAZY) private Board board; @CreationTimestamp private Timestamp createdAt; // Builder 만들기 : 생성자 만들고 @Builder 걸기 @Builder public Love(Integer id, User user, Board board, Timestamp createdAt) { this.id = id; this.user = user; this.board = board; this.createdAt = createdAt; } }
notion image
 

2. 더미데이터 추가

insert into love_tb(board_id, user_id, created_at) values (5, 1, now()); insert into love_tb(board_id, user_id, created_at) values (5, 2, now()); insert into love_tb(board_id, user_id, created_at) values (4, 1, now());
 

3. 제약조건 위배 시 제대로 오류가 터지는 지 확인하기

  1. 같은 게시글에 같은 유저가 좋아요를 여러 번 했다고 상황을 가정하자.
    1. notion image
  1. 에러
    1. notion image
      notion image
      notion image
 

3. 화면과 Controller만 가지고 화면에 뿌리는 것 먼저!

  • DB에는 아직 손대지 않고!!! 화면에 대한 이해가 끝나면 다음에는 DB까지 고려
  • 일단 ssar이면 true → 무조건 좋아요
  • 좋아요 기능 전체 X → 먼저 하트 색깔부터 제어

1. BoardResponse.DetailDTO

  1. isLove : 좋아요 하면 true, 좋아요를 취소하면 false
  1. loveCount : 게시글에 대한 좋아요 개수
notion image
 

2. board/detail.mustache

notion image
  • 비추천 방식 (가독성이 떨어짐)
    • notion image
 

3.

notion image
notion image
 

4. 처음 만들어보는 것에 대해서

given 데이터를 이용하자
여러가지 중에서 하나씩 빼보고 샘플링 해보면서
기능을 분리해서 생각하자
 

4. 좋아요 하트 처리

1. love entity와 다름

// 나는 1번이고 5번 게시물로 갈거야 // 좋아요는 true가 나와야 됨 => select * from love_tb where board_id=5 and user_id = 1;
notion image
근데 이 데이터를 들고 가고 싶은게 아니라 true를 들고 가고 싶은 것!
 
notion image
근데 love entity와 다름
true로 하면 object 배열로 받아야됨 그냥 원래 데이터 받아서 매핑 받고 처리하는게ㅔ 낫다
 

2. Repository

public Love findByUserIdAndBoardId(int userId, int boardId) { Query query = em.createQuery("select lo from Love lo where lo.user.id = :userId and lo.board.id = :boardId", Love.class); query.setParameter("userId", userId); query.setParameter("boardId", boardId); return (Love) query.getSingleResult(); // unique 제약조건이므로 동일한 데이터가 있을 수 없기 때문에 Single }
 

3. BoardService

notion image
 

4. BoardResponse.DetailDTO

notion image
 

5. 3번 게시물을 ssar로 로그인해서 들어가면

notion image
notion image
 

6. 5번글은 비공개이므로 더미 데이터를 잘 못 만들었음

notion image
 

5. 좋아요 cnt - count(*)

Respository의 메서드가 하나의 목적으로만 사용되는 것보다는 재사용 가능한 것이 좋기 때문에 count로 좋아요 수를 구하는 것보다는 List.size() 방식을 사용하자!
 

1. 좋아요 count하는 쿼리 먼저 짜기

SELECT count(*) FROM LOVE_TB where board_id = 5;
 

2. LoveRepository

public int countByBoardId(int boardId) { Query query = em.createQuery("SELECT count(lo) FROM Love lo where lo.board.id = :boardId"); query.setParameter("boardId", boardId); Long loveCount = (Long) query.getSingleResult(); return loveCount.intValue(); }
 

3. BoardService

notion image
 

4. BoardResponse.DetailDTO

notion image
 

5. LoveRepositoryTest

@Import(LoveRepository.class) @DataJpaTest public class LoveRepositoryTest { @Autowired private LoveRepository loveRepository; @Test public void findByBoardId_test() { // given Integer boardId = 3; // when Integer loveCount = loveRepository.countByBoardId(boardId); System.out.println(loveCount); } }
  1. given data인 boardId = 3인 경우
    1. notion image
  1. given data인 boardId = 4인 경우
    1. notion image
  1. given data인 boardId = 5인 경우
    1. notion image
 

6. 좋아요 cnt - List.size()

1. LoveRepository

public List<Love> findByBoardId(int boardId) { Query query = em.createQuery("SELECT lo FROM Love lo where lo.board.id = :boardId"); query.setParameter("boardId", boardId); List<Love> loves = query.getResultList(); return loves; }
 

2. BoardService

public BoardResponse.DetailDTO 글상세보기(int id, Integer userId) { // Board board = boardRepository.findById(id); // LAZY 로딩이므로 Board만 조회해서 board 정보 밖에 없다. // board.getUser().getEmail(); // 원래 null인데 lazy로딩이 발동해서 해당 유저 id로 select가 발동해서 값을 넣어준다. -> 비효율적이므로 안쓴다! Board board = boardRepository.findByIdJoinUser(id); Love love = loveRepository.findByUserIdAndBoardId(userId, id); // (userId, boardId) Boolean isLove = love == null ? false : true; List<Love> loves = loveRepository.findByBoardId(id); BoardResponse.DetailDTO detailDTO = new BoardResponse.DetailDTO(board, userId, isLove, loves.size()); return detailDTO; }
 

3. BoardResponse.DetailDTO

package shop.mtcoding.blog.board; import lombok.Data; import java.sql.Timestamp; public class BoardResponse { @Data public static class DetailDTO { private Integer id; private String title; private String content; private Boolean isPublic; private Boolean isOwner; // 값이 안 들어갈 경우: Boolean - null / boolean - 0 private Boolean isLove; private Integer loveCount; private String username; // User 객체를 다 들고 갈 필요X private Timestamp createdAt; // model에 있는 것을 옮기는 것 // 깊은 복사 : 객체를 그대로 가져와서 getId 등으로 넣는게 낫다! public DetailDTO(Board board, Integer sessionUserId, Boolean isLove, Integer loveCount) { this.id = board.getId(); this.title = board.getTitle(); this.content = board.getContent(); this.isPublic = board.getIsPublic(); this.isOwner = sessionUserId == board.getUser().getId(); this.username = board.getUser().getUsername(); this.createdAt = board.getCreatedAt(); this.isLove = isLove; this.loveCount = loveCount; } } }
 

결과

notion image
notion image
notion image
Share article

jay0628