[Spring Boot] 43. 스프링부트 블로그 v2 (JPA) (13) 게시글 좋아요 4

김주희's avatar
Apr 08, 2025
[Spring Boot] 43. 스프링부트 블로그 v2 (JPA) (13) 게시글 좋아요 4

1. 클라이언트 측

0. 아직 수정이 필요한 코드

{{> layout/header}} <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="likeIcon" class="fa fa-heart" style="font-size:20px; color:red" onclick="deleteLove({{model.loveId}})"></i> {{/model.isLove}} {{^model.isLove}} <i id="likeIcon" class="fa fa-heart" style="font-size:20px; color:black" onclick="saveLove()"></i> {{/model.isLove}} <span class="ms-1"><b id="likeCount">{{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"> <!-- 댓글아이템 --> <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">cos</div> <div>댓글 내용입니다</div> </div> <form action="/reply/1/delete" method="post"> <button class="btn">🗑</button> </form> </div> <!-- 댓글아이템 --> <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">ssar</div> <div>댓글 내용입니다</div> </div> <form action="/reply/1/delete" method="post"> <button class="btn">🗑</button> </form> </div> </div> </div> </div> <script> let boardId = {{model.id}}; 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(); // { loveId, loveCount } // DOM 업데이트 let icon = document.querySelector('#likeIcon'); let likeCountSpan = document.querySelector('#likeCount'); icon.style.color = 'red'; icon.setAttribute('onclick', `deleteLove(${responseBody.loveId})`); likeCountSpan.textContent = responseBody.loveCount; } async function deleteLove(loveId) { let response = await fetch(`/love/${loveId}`, { method: "DELETE" }); let responseBody = await response.json(); // { loveCount } // DOM 업데이트 let icon = document.querySelector('#likeIcon'); let likeCountSpan = document.querySelector('#likeCount'); icon.style.color = 'black'; icon.setAttribute('onclick', `saveLove()`); likeCountSpan.textContent = responseBody.loveCount; } </script> {{> layout/footer}}
notion image
notion image
 

1. JS로 화면에 전달된 데이터 사용하는 방법

  1. JS에다가 mustache 문법 절대 사용 금지!!
    1. js file로 분리하면 인식 안됨
    2. 바깥에다가 심어놓고 js로 땡기는 방식을 사용하자
let boardId = {{model.id}}; (X)
 
  1. JS 함수의 매개변수의 인수로 전달한다.
    1. notion image
      notion image
 

2. 화면 제일 위에 input 태그로 boardId 고정하기 (추천)

  1. boardId는 상세보기 화면에서는 그 값이 고정되므로 input 태그를 제일 위에 전역으로 올려 두고 script에서 땡겨서 쓰는 것이 낫다.
  1. loveId는 게시글 하나에 대해서 고정된 값이 아니기 때문에 전역으로 설정하지 않는다.
  1. script 안에서도 함수 안에 바로 넣는 것 보다는 나중에 다른 위치에서 필요할 때 또 꺼내 써야 하는 불편함이 있기 때문에 함수 밖에 두는 것이 좋다.
notion image
notion image
 
notion image
 

3. Dataset

  1. dataset 속성으로 bordId와 loveId를 icon 태그 안에 심는다.
    1. notion image
      notion image
 
  1. 3번 게시글로 들어가면 터진다 → {{^model.isLove}} 일때는 data-loveId 없어도 되니까 삭제!
notion image
 
  1. 최종적으로 delete 할때만 loveId와 boardId를 받고 save할때는 boardId만 dataset으로 dom 요소에 저장한다.
notion image
 
  1. js 코드
    1. notion image
      notion image
 
 

4. 최종

3가지 방법을 다 알고 있어야 혹시나 안되는 경우 다른 방법 사용 가능하다!
notion image
notion image
notion image
notion image
 

2. 서버를 완성하자

1. LoveRequest

  1. 원래 JPA를 사용하고 객체를 빌더로 만들 때에는 객체를 주입하는 게 맞지만 여기서는 일단 id를 찾아서 대입한다.
package shop.mtcoding.blog.love; import lombok.Data; import shop.mtcoding.blog.board.Board; import shop.mtcoding.blog.user.User; public class LoveRequest { @Data public static class SaveDTO { private Integer boardId; public Love toEntity(Integer sessionUserId) { return Love.builder() .board(Board.builder().id(boardId).build()) .user(User.builder().id(sessionUserId).build()) .build(); } } }
 

2. LoveResponse

  1. 좋아요 → loveId와 loveCount 리턴
  1. 좋아요 취소 → loveCount만 리턴
package shop.mtcoding.blog.love; import lombok.Data; public class LoveResponse { @Data public static class SaveDTO { private Integer loveId; private Long loveCount; // @AllArg~을 안하고 생성자 직접 만드는 이유 = 커스터마이징 가능하니까 public SaveDTO(Integer loveId, Long loveCount) { this.loveId = loveId; this.loveCount = loveCount; } } @Data public static class DeleteDTO { private Long loveCount; public DeleteDTO(Long loveCount) { this.loveCount = loveCount; } } }
 

3. LoveController

  1. LoveRequest와 LoveResponse 둘다 SaveDTO 클래스라고 이름을 지었기 때문에 변수명은 request의 경우 reqDTO, response의 경우 respDTO라고 하는 것이 구분하기 좋다.
package shop.mtcoding.blog.love; import jakarta.servlet.http.HttpSession; import lombok.RequiredArgsConstructor; import org.springframework.web.bind.annotation.*; import shop.mtcoding.blog._core.Resp; import shop.mtcoding.blog.user.User; @RequiredArgsConstructor @RestController // 여기서는 ajax로 데이터만 return하는 controller이므로 그냥 RestController라고 하자 public class LoveController { private final LoveService loveService; private final HttpSession session; @PostMapping("/love") public Resp<?> saveLove(@RequestBody LoveRequest.SaveDTO reqDTO) { // @RestController이므로 @ResponseBody 안붙여도 된다. User sessionUser = (User) session.getAttribute("sessionUser"); if (sessionUser == null) throw new RuntimeException("인증이 필요합니다."); LoveResponse.SaveDTO respDTO = loveService.좋아요(reqDTO, sessionUser.getId()); return Resp.ok(respDTO); } @DeleteMapping("/love/{id}") public Resp<?> deleteLove(@PathVariable("id") Integer id) { User sessionUser = (User) session.getAttribute("sessionUser"); if (sessionUser == null) throw new RuntimeException("인증이 필요합니다."); LoveResponse.DeleteDTO respDTO = loveService.좋아요취소(id); return Resp.ok(respDTO); } }
 

4. LoveService

  1. persist 하기 위해서는 객체가 들어가야 하기 때문에 원래는 id가 아닌 User 객체인 sessionUser가 들어가야 하지만 여기서는 id를 사용한다.
package shop.mtcoding.blog.love; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @RequiredArgsConstructor // DI @Service // IoC public class LoveService { private final LoveRepository loveRepository; @Transactional public LoveResponse.SaveDTO 좋아요(LoveRequest.SaveDTO reqDTO, Integer sessionUserId) { Love lovePS = loveRepository.save(reqDTO.toEntity(sessionUserId)); Long loveCount = loveRepository.findByBoardId(reqDTO.getBoardId()); return new LoveResponse.SaveDTO(lovePS.getId(), loveCount); } @Transactional public LoveResponse.DeleteDTO 좋아요취소(Integer id) { Love lovePs = loveRepository.findById(id); if (lovePs == null) throw new RuntimeException("취소할 수 있는 좋아요가 없습니다."); Integer boardId = lovePs.getBoard().getId(); loveRepository.deleteById(id); Long loveCount = loveRepository.findByBoardId(boardId); return new LoveResponse.DeleteDTO(loveCount); } }
 

5. LoveRepository

package shop.mtcoding.blog.love; import jakarta.persistence.EntityManager; import jakarta.persistence.Query; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Repository; @RequiredArgsConstructor @Repository public class LoveRepository { private final EntityManager em; 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(); // unique 제약조건이므로 동일한 데이터가 있을 수 없기 때문에 Single } catch (Exception e) { return null; } } 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 save(Love love) { em.persist(love); return love; // PK가 담긴 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); } }
Share article

jay0628