42. 좋아요하기, 취소하기

yuzu sim's avatar
Feb 14, 2024
42. 좋아요하기, 취소하기

AJAX 통신 규칙 = CSR(앱) 프로젝트 할 때 참고 사항

  • json데이터 기반으로 통신하기 위해 ajax를 해야함
  • 실패하면 반드시 헤더에 상태코드를 줘야 fail이 뜸
  • ajax통신은 데이터만 교환
  • select(get)
  • delete→ 삭제해서 행이 지워짐(null로 하면 됨)
  • post, put(반드시 생긴 데이터를 바디에 담아 반환하는 것이 규칙)→ 아니면 화면에 못 그려줌
  • put: 추가 요청, 추가된 row의 pk를 알 수 없음
반드시 하나의 트랜잭션에 묶여있어야 함
아니면 일관성이 깨질 수 있음
  • 추가된 row를 전체 받아와야함
  • put: 수정 요청, pk를 알고 있음
  • 수정될 데이터 바디값, where에 pk값을 알고 있기 때문에 답으로 null을 줘도 그릴 수 있음
  • 안 받아도 되나 변경된 row를 통째로 받는 것이 규칙임
 

1. detail.mustache에서 좋아요 부분 추가하기

  • onclick 추가하기
  • post, put만 바디가 있음
  • 화면에 하나밖에 안뜨니까 둘 다 love로 id만들어도 됨
{{> layout/header}} <div class="container p-5"> {{#board.boardOwner}} <!-- 수정삭제버튼 --> <div class="d-flex justify-content-end"> <a href="/board/{{board.id}}/updateForm" class="btn btn-warning me-1">수정</a> <form action="/board/{{board.id}}/delete" method="post"> <button class="btn btn-danger">삭제</button> </form> </div> {{/board.boardOwner}} <div class="d-flex justify-content-end"> <b>작성자</b> : {{board.username}} </div> <!-- 게시글내용 --> <div> <h2><b>{{board.title}}</b></h2> <hr/> <div class="m-4 p-2"> {{board.content}} </div> </div> <!-- 좋아요 / 따로 조회해서 들고오기--> <div> {{#love.isLove}} <i id="love" onclick="loveDel()" class="fas fa-heart my-cursor text-danger"></i> {{/love.isLove}} {{^love.isLove}} <i id="love" onclick="loveAdd()" class="far fa-heart my-cursor"></i> {{/love.isLove}} <input type="hidden" id="love-id" value="{{love.id}}"> <span>{{love.loveCount}}</span> </div> <!-- 댓글 --> <div class="card mt-3"> <!-- 댓글등록 --> <div class="card-body"> <form action="/reply/save" method="post"> <input type="hidden" name="boardId" value="{{board.id}}"> <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"> {{#replyList}} <!-- 댓글아이템 --> <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">{{username}}</div> <div>{{comment}}</div> </div> {{#replyOwner}} <form action="/reply/{{id}}/delete" method="post"> <button class="btn">🗑</button> </form> {{/replyOwner}} </div> {{/replyList}} </div> </div> </div> <script> function loveDel() { //좋아요 취소 let loveId = $("#love-id").val(); $.ajax({ url: "/api/love" + loveID, type: "delete" // 바디가 없음 }).done((res) => { $("#love").removeClass("fas"); $("#love").removeClass("test-danger"); $("#love").addClass("fas"); }).fail((res) => { alert(res.responseJSON.msg) }) } function loveAdd() { //좋아요 하기 } </script> {{> layout/footer}}
 

2. LoveRequest만들어서 SaveDTO 만들기

package shop.mtcoding.blog.love; import lombok.Data; public class LoveRequest { @Data public static class SaveDTO { private Integer boardId; } }
 

3. LoveController 만들어서 delete 주소 만들기

  • 상태코드 넣어주기
package shop.mtcoding.blog.love; import jakarta.servlet.http.HttpServletResponse; import jakarta.servlet.http.HttpSession; import lombok.RequiredArgsConstructor; import org.springframework.web.bind.annotation.*; import shop.mtcoding.blog._core.util.ApiUtil; import shop.mtcoding.blog.user.User; @RequiredArgsConstructor @RestController public class LoveController { private final LoveRepository loveRepository; private final HttpSession session; @DeleteMapping("/api/love/{id}") public ApiUtil<?> delete(@PathVariable Integer id, HttpServletResponse response){ // 1. 인증 체크 User sessionUser = (User) session.getAttribute("sessionUser"); if (sessionUser == null) { response.setStatus(401); return new ApiUtil<>(401, "인증안됨"); } // 2. 권한 체크 Love love = loveRepository.findById(id); if(love.getUserId() != sessionUser.getId()){ response.setStatus(403); return new ApiUtil<>(403, "권한없음"); } loveRepository.deleteById(id); return new ApiUtil<>(null); } @PostMapping("/api/love") public ApiUtil<?> save(@RequestBody LoveRequest.SaveDTO requestDTO, HttpServletResponse response){ // 1. 인증 체크 User sessionUser = (User) session.getAttribute("sessionUser"); if (sessionUser == null) { response.setStatus(401); return new ApiUtil<>(401, "인증안됨"); } int loveId = loveRepository.save(requestDTO, sessionUser.getId()); return new ApiUtil<>(loveId); } }
 

4. LoveRepository에 findById()만들기

  • BoardCtroller에 findById() 복붙해서 만들기
  • 같은 트랜잭션 안에 들어가야함
밖에 있으면 다른 값이 튀어나올 수 있음
  • max값 조회해서 삽입한 현재값으로 id를 받아와야 삭제도 가능함
package shop.mtcoding.blog.love; import jakarta.persistence.EntityManager; import jakarta.persistence.Query; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Repository; import shop.mtcoding.blog.board.Board; @RequiredArgsConstructor @Repository public class LoveRepository { private final EntityManager em; public Love findById(int id) { Query query = em.createNativeQuery("select * from love_tb where id = ?", Love.class); query.setParameter(1, id); Love love = (Love) query.getSingleResult(); return love; } public LoveResponse.DetailDTO findLove(int boardId) { // 카운트만 받기 String q = """ SELECT count(*) loveCount FROM love_tb WHERE board_id = ?; """; Query query = em.createNativeQuery(q); query.setParameter(1, boardId); // 한건만 받을때는 바로 받기 Long loveCount = (Long) query.getSingleResult(); Integer id = 0; Boolean isLove = false; System.out.println("id : " + id); System.out.println("isLove : " + isLove); System.out.println("loveCount : " + loveCount); LoveResponse.DetailDTO responseDTO = new LoveResponse.DetailDTO( id, isLove, loveCount ); return responseDTO; } public LoveResponse.DetailDTO findLove(int boardId, int sessionUserId) { String q = """ SELECT id, CASE WHEN user_id IS NULL THEN FALSE ELSE TRUE END AS isLove, (SELECT COUNT(*) FROM love_tb WHERE board_id = ?) AS loveCount FROM love_tb WHERE board_id = ? AND user_id = ?; """; Query query = em.createNativeQuery(q); query.setParameter(1, boardId); query.setParameter(2, boardId); query.setParameter(3, sessionUserId); Integer id = null; Boolean isLove = null; Long loveCount = null; // long은 null을 사용할 수 없음, Long은 1L로 받아야 함 try { // 정상적으로 받기 Object[] row = (Object[]) query.getSingleResult(); id = (Integer) row[0]; isLove = (Boolean) row[1]; loveCount = (Long) row[2]; } catch (Exception e) { // 댓글이 없어서 터지면 0으로 초기화 id = 0; isLove = false; loveCount = 0L; } System.out.println("id : " + id); System.out.println("isLove : " + isLove); System.out.println("loveCount : " + loveCount); LoveResponse.DetailDTO responseDTO = new LoveResponse.DetailDTO( id, isLove, loveCount ); return responseDTO; } }
  • 쿼리 수정하기 방금 insert된 데이터 전체 응답하기
  • insert된 row 자체를 리턴해줌 : pk를 알 기 위해서임
->insert 시점에서는 알 수 없음
package shop.mtcoding.blog.love; import jakarta.persistence.EntityManager; import jakarta.persistence.Query; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Repository; import org.springframework.transaction.annotation.Transactional; @RequiredArgsConstructor @Repository public class LoveRepository { private final EntityManager em; public Love findById(int id) { Query query = em.createNativeQuery("select * from love_tb where id = ?", Love.class); query.setParameter(1, id); Love love = (Love) query.getSingleResult(); return love; } @Transactional public void deleteById(int id) { Query query = em.createNativeQuery("delete from love_tb where id = ?"); query.setParameter(1, id); query.executeUpdate(); } public LoveResponse.DetailDTO findLove(int boardId) { String q = """ SELECT count(*) loveCount FROM love_tb WHERE board_id = ?; """; Query query = em.createNativeQuery(q); query.setParameter(1, boardId); // 한건만 받을때는 바로 받기 Long loveCount = (Long) query.getSingleResult(); Integer id = 0; Boolean isLove = false; System.out.println("id : " + id); System.out.println("isLove : " + isLove); System.out.println("loveCount : " + loveCount); LoveResponse.DetailDTO responseDTO = new LoveResponse.DetailDTO( id, isLove, loveCount ); return responseDTO; } public LoveResponse.DetailDTO findLove(int boardId, int sessionUserId) { String q = """ SELECT id, CASE WHEN user_id IS NULL THEN FALSE ELSE TRUE END AS isLove, (SELECT COUNT(*) FROM love_tb WHERE board_id = ?) AS loveCount FROM love_tb WHERE board_id = ? AND user_id = ?; """; Query query = em.createNativeQuery(q); query.setParameter(1, boardId); query.setParameter(2, boardId); query.setParameter(3, sessionUserId); Integer id = null; Boolean isLove = null; Long loveCount = null; try { Object[] row = (Object[]) query.getSingleResult(); id = (Integer) row[0]; isLove = (Boolean) row[1]; loveCount = (Long) row[2]; } catch (Exception e) { id = 0; isLove = false; loveCount = 0L; } System.out.println("id : " + id); System.out.println("isLove : " + isLove); System.out.println("loveCount : " + loveCount); LoveResponse.DetailDTO responseDTO = new LoveResponse.DetailDTO( id, isLove, loveCount ); return responseDTO; } @Transactional // insert된 row 자체를 리턴해줌->insert 시점에서는 알 수 없음 /pk를 알 기 위해서임 public Love save(LoveRequest.SaveDTO requestDTO, int sessionUserId) { Query query = em.createNativeQuery("insert into love_tb(board_id, user_id, created_at) values(?,?, now())"); query.setParameter(1, requestDTO.getBoardId()); query.setParameter(2, sessionUserId); query.executeUpdate(); // 같은 트랜잭션 안에 들어가야함 Query q = em.createNativeQuery("select select * from love_tb where id=(max(id) from love_tb))", Love.class); // max값 조회해서 삽입한 현재값으로 id를 받아와야 삭제도 가능함 Love love = (Love) q.getSingleResult(); return love; } }
 

5. LoveController에 save()수정하기

package shop.mtcoding.blog.love; import jakarta.servlet.http.HttpServletResponse; import jakarta.servlet.http.HttpSession; import lombok.RequiredArgsConstructor; import org.springframework.web.bind.annotation.*; import shop.mtcoding.blog._core.util.ApiUtil; import shop.mtcoding.blog.user.User; @RequiredArgsConstructor @RestController public class LoveController { private final LoveRepository loveRepository; private final HttpSession session; @DeleteMapping("/api/love/{id}") public ApiUtil<?> delete(@PathVariable Integer id, HttpServletResponse response){ // 1. 인증 체크 User sessionUser = (User) session.getAttribute("sessionUser"); if (sessionUser == null) { response.setStatus(401); return new ApiUtil<>(401, "인증안됨"); } // 2. 권한 체크 Love love = loveRepository.findById(id); if(love.getUserId() != sessionUser.getId()){ response.setStatus(403); return new ApiUtil<>(403, "권한없음"); } loveRepository.deleteById(id); return new ApiUtil<>(null); } @PostMapping("/api/love") // insert해서 insert된 데이터를 바디에 돌려주는 것 public ApiUtil<?> save(@RequestBody LoveRequest.SaveDTO requestDTO, HttpServletResponse response){ // 1. 인증 체크 User sessionUser = (User) session.getAttribute("sessionUser"); if (sessionUser == null) { response.setStatus(401); return new ApiUtil<>(401, "인증안됨"); } Love love = loveRepository.save(requestDTO, sessionUser.getId()); return new ApiUtil<>(love); // love를 바디에 담기 } }
 

6. detail.mustache 수정하기

{{> /layout/header}} <div class="container p-5"> {{#board.boardOwner}} <!-- 수정삭제버튼 --> <div class="d-flex justify-content-end"> <a href="/board/{{board.id}}/updateForm" class="btn btn-warning me-1">수정</a> <form action="/board/{{board.id}}/delete" method="post"> <button class="btn btn-danger">삭제</button> </form> </div> {{/board.boardOwner}} <div class="d-flex justify-content-end"> <b>작성자</b> : {{board.username}} </div> <!-- 게시글내용 --> <div> <h2><b>{{board.title}}</b></h2> <hr/> <div class="m-4 p-2"> {{board.content}} </div> </div> <!--좋아요 부분--> <div> {{#love.isLove}} <i id="love" onclick="loveToggle({{board.id}})" class="fas fa-heart text-danger"></i> {{/love.isLove}} {{^love.isLove}} <i id="love" onclick="loveToggle({{board.id}})" class="far fa-heart"></i> {{/love.isLove}} <input type="hidden" id="love-id" value="{{love.id}}"> <span id="loveCount">{{love.loveCount}}</span> </div> <script> function loveToggle(boardId) { // 좋아요 하기 if($("#love").hasClass("far")){ let love = { boardId: boardId } $.ajax({ url: "/api/love", type: "post", data: JSON.stringify(love), contentType: "application/json; charset=utf-8" }).done((res) => { console.log(res); $("#love").removeClass("far"); $("#love").addClass("text-danger"); $("#love").addClass("fas"); let loveCount = $("#loveCount").text(); $("#loveCount").text(Number(loveCount) + 1); // 좋아요가 되면 love id를 변경해야함 $("#love-id").val(res.body); }).fail((res) => { alert(res.responseJSON.msg); }); }else{ // 좋아요 취소 하기 let loveId = $("#love-id").val(); $.ajax({ url: "/api/love/" + loveId, type: "delete" }).done((res) => { console.log(res); $("#love").removeClass("fas"); $("#love").removeClass("text-danger"); $("#love").addClass("far"); let loveCount = $("#loveCount").text(); $("#loveCount").text(Number(loveCount) - 1); // 좋아요가 취소되면 love id 초기화 $("#love-id").val(0); }).fail((res) => { alert(res.responseJSON.msg); }); } } </script> <!-- 댓글 --> <div class="card mt-3"> <!-- 댓글등록 --> <div class="card-body"> <form action="/reply/save" method="post"> <input type="hidden" name="boardId" value="{{board.id}}"> <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"> {{#replyList}} <!-- 댓글아이템 --> <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">{{username}}</div> <div>{{comment}}</div> </div> {{#replyOwner}} <form action="/reply/{{id}}/delete" method="post"> <button class="btn">🗑</button> </form> {{/replyOwner}} </div> {{/replyList}} </div> </div> </div> {{> /layout/footer}}
  • 계속 눌러지는것 잡아야함 그래서 del, add가 아니라 toggle로 만드는 것이 좋음
notion image
notion image
Share article

Coding_study