
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로 만드는 것이 좋음


Share article