[스프링부트] 21. Blog v2 : Like

문정준's avatar
Apr 09, 2025
[스프링부트] 21. Blog v2 : Like
 

1. 좋아요 테이블 생성

  • Like
@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) private User user; @ManyToOne(fetch = FetchType.LAZY) private Board board; @CreationTimestamp private Timestamp createdAt; @Builder public Love(Integer id, User user, Board board, Timestamp createdAt) { this.id = id; this.user = user; this.board = board; this.createdAt = createdAt; } }
  • data.sql
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 (4, 2, now()); insert into love_tb(board_id, user_id, created_at) values (4, 1, now());
 

2. 좋아요 집계

  • LikeRepository
    • findByBoardId : 글에 걸린 좋아요 개수
    • findByUserIdAndBoardId : 로그인 한 유저가 해당 글에 좋아요를 눌렀는지 안눌렀는지 확인
    • save : 좋아요 추가 및 객체 반환 (좋아요 개수 갱신용)
    • deleteById : 좋아요 id를 이용하여 좋아요 삭제
    • findById : 좋아요 id를 이용하여 love 객체 반환 (좋아요 개수 갱신용)
public Long findByBoardId(int boardId) { Query query = em.createQuery("select count(lo) from Love lo where lo.board.id = :boardId"); query.setParameter("boardId", boardId); Long count = (Long) query.getSingleResult(); return count; } public Love findByUserIdAndBoardId(Integer userId, Integer 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); try { return (Love) query.getSingleResult(); } catch (Exception e) { return null; } } public Love save(Love love) { em.persist(love); return love; } public void deleteById(Integer id) { em.createQuery("delete from Love lo where lo.id = :id") .setParameter("id", id) .executeUpdate(); } public Love findById(Integer id) { return em.find(Love.class, id); }
  • BoardService
    • 게시글의 상세보기에 출력 → BoardResponse에 좋아요 저장
public BoardResponse.DetailDTO 글상세보기(Integer id, Integer userId) { Board board = boardRepository.findByIdJoinUser(id); Love love = loveRepository.findByUserIdAndBoardId(userId, id); Long loveCount = loveRepository.findByBoardId(id); Integer loveId = love == null ? null : love.getId(); Boolean isLove = love == null ? false : true; BoardResponse.DetailDTO detailDTO = new BoardResponse.DetailDTO(board, userId, isLove, loveCount.intValue(), loveId); return detailDTO; }
  • BoardResponse
    • isLove : 좋아요를 누른 상태 표시
    • loveCount : 해당 글의 좋아요 개수 표시
    • loveId : 좋아요 추가 / 취소를 위한 Id 참조
// 상세보기 화면에 필요한 데이터 @Data public static class DetailDTO { private Integer id; private String title; private String content; private Boolean isPublic; private Boolean isOwner; private Boolean isLove; private Integer loveCount; private String username; private Timestamp createdAt; private Integer loveId; public DetailDTO(Board board, Integer sessionUserId, Boolean isLove, Integer loveCount, Integer loveId) { 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; this.loveId = loveId; }
 

3. 좋아요 DTO

  • LoveRequest
public static class SaveDTO { private Integer boardId; public Love toEntity(User sessionUser) { return Love.builder() .board(Board.builder().id(boardId).build()) .user(sessionUser) .build(); } }
  • LoveResponse
@Data public static class SaveDTO { private Integer loveId; private Integer loveCount; public SaveDTO(Integer loveId, Integer loveCount) { this.loveId = loveId; this.loveCount = loveCount; } } @Data public static class DeleteDTO { private Integer loveCount; public DeleteDTO(Integer loveCount) { this.loveCount = loveCount; } }
 

4. 좋아요 JS

{{> layout/header}} <input type="hidden" id="boardId" value="{{model.id}}"> <div class="container p-5"> <!-- 수정삭제버튼 --> {{#model.isOwner}} <div class="d-flex justify-content-end"> <a href="/board/{{model.id}}/update-form" class="btn btn-warning me-1">수정</a> <form action="/board/{{model.id}}/delete" method="post"> <button class="btn btn-danger">삭제</button> </form> </div> {{/model.isOwner}} <div class="d-flex justify-content-end"> <b>작성자</b> : {{model.username}} </div> <!-- 게시글내용 --> <div> <h2><b>{{model.title}}</b></h2> <hr/> <div class="m-4 p-2"> {{model.content}} </div> </div> <!-- AJAX 좋아요 영역 --> <div class="my-3 d-flex align-items-center"> {{#model.isLove}} <i id="loveIcon" class="fa fa-heart" style="font-size:20px; color:red" onclick="deleteLove({{model.loveId}})"></i> {{/model.isLove}} {{^model.isLove}} <i id="loveIcon" class="fa fa-heart" style="font-size:20px; color:black" onclick="saveLove()"></i> {{/model.isLove}} <span class="ms-1"><b id="loveCount">{{model.loveCount}}</b>명이 이 글을 좋아합니다</span> </div> <!-- 댓글 --> <div class="card mt-3"> <!-- 댓글등록 --> <div class="card-body"> <form action="/reply/save" method="post"> <textarea class="form-control" rows="2" name="comment"></textarea> <div class="d-flex justify-content-end"> <button type="submit" class="btn btn-outline-primary mt-1">댓글등록</button> </div> </form> </div> <!-- 댓글목록 --> <div class="card-footer"> <b>댓글리스트</b> </div> <div class="list-group"> <!-- 댓글아이템 --> {{#model.replies}} <div class="list-group-item d-flex justify-content-between align-items-center"> <div class="d-flex"> <div class="px-1 me-1 bg-primary text-white rounded">{{user.username}}</div> <div>{{content}}</div> </div> <form action="/reply/{{id}}/delete" method="post"> <button class="btn">🗑</button> </form> </div> {{/model.replies}} </div> </div> </div> <script> // setInterval(()=>{ // location.reload(); // }, 1000*10); let boardId = document.querySelector("#boardId").value; async function saveLove() { let requestBody = {boardId: boardId}; let response = await fetch(`/love`, { method: "POST", body: JSON.stringify(requestBody), headers: {"Content-Type": "application/json"} }); let responseBody = await response.json(); // response.text() (X) console.log(responseBody); // DOM 업데이트 let icon = document.querySelector('#loveIcon'); let loveCount = document.querySelector('#loveCount'); icon.style.color = 'red'; icon.setAttribute('onclick', `deleteLove(${responseBody.body.loveId})`); loveCount.innerHTML = responseBody.body.loveCount; } async function deleteLove(loveId) { let response = await fetch(`/love/${loveId}`, { method: "DELETE" }); let responseBody = await response.json(); // response.text() (X) console.log(responseBody); // DOM 업데이트 let icon = document.querySelector('#loveIcon'); let loveCount = document.querySelector('#loveCount'); icon.style.color = 'black'; icon.setAttribute('onclick', `saveLove()`); loveCount.innerHTML = responseBody.body.loveCount; } </script> {{> layout/footer}}
 

1. 화면의 변수를 매개변수로 전달

  • 함수마다 id를 각각 받아야 함
notion image
 

2. input 태그에 id 숨겨놓기

  • boardId의 경우 한 게시글 내에서 좋아요를 누르므로 변하지 않음
  • 미리 값을 받은 후 document.querySelector(”#boardId”).value로 값을 땡겨 씀
notion image
notion image
 

3. dataset 활용

  • 태그 내부에 data-key=”value” 형태의 dataset 추가
  • document.querySelector(”#id”).dataset.key를 사용하여 해당 태그 내 dataset 사용 가능
notion image
notion image
 
✏️

효율적 방법?

  • Dataset : 데이터 활용이 쉬움, 코드가 번잡해지는 단점
  • Input Tag : 태그를 hidden으로 숨겨서 관리, 보안성 면에서 적합
  • 전역 변수 : 간단한 변수 선언, 보안에서 위험
 
 

5. 결과

  • 좋아요가 걸려있지 않은 글 : 하트가 검정색으로 표시됨
    • 누르면 좋아요 개수가 올라감
notion image
  • 좋아요가 걸린 글 : 하트가 빨간색으로 표시됨
    • 누르면 좋아요 개수가 내려감
notion image
 
 
Share article

sxias