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}}

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


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



3. Dataset
- dataset 속성으로 bordId와 loveId를 icon 태그 안에 심는다.


- 3번 게시글로 들어가면 터진다 →
{{^model.isLove}}일때는 data-loveId 없어도 되니까 삭제!

- 최종적으로 delete 할때만 loveId와 boardId를 받고 save할때는 boardId만 dataset으로 dom 요소에 저장한다.

- js 코드


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




2. 서버를 완성하자
1. LoveRequest
- 원래 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
- 좋아요 → loveId와 loveCount 리턴
- 좋아요 취소 → 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
- 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
- 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