41. 좋아요 구현 : 목록 만들기

yuzu sim's avatar
Feb 14, 2024
41. 좋아요 구현 : 목록 만들기

1. 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> <i class="far fa-heart my-cursor"></i> <span>10</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> {{> layout/footer}}
 

2. love 테이블 관계

  • 관계 : N:N
user가 여러 게시글을 보고 좋아요를 누를 수 있음 → 1:N
게시글 하나를 여러 user가 좋아요를 누를 수 있음 → N:1
중간 테이블이 필요함
  • 외래키 두 개가 필요함 : user_id, board_id
  • 필요한 것 : user_id, board_id가 중복될 수 없음
id
user_id
board_id
created_at
1
1
1
2
1
1
중복될 수 없음
3
1
2
→ user_id, board_id가 중복되면 터뜨려야 함
 
* 팔로우의 경우
ifrome_user_id : 팔로우 누르는 user
to_user_id : 팔로우 당한 user
id
frome_user_id
to_user_id
created_at
1
1
1
2
1
1
중복될 수 없음
3
1
2
 

3. love 패키지 만들어서 Love 클래스 만들기

  • love_tb 만들기
package shop.mtcoding.blog.love; import jakarta.persistence.*; import lombok.Data; import java.sql.Timestamp; @Table(name="love_tb") @Data @Entity // 테이블 생성하기 위해 필요한 어노테이션 public class Love { @Id // PK 설정 @GeneratedValue(strategy = GenerationType.IDENTITY) // auto_increment 전략 private Integer id; private Integer boardId; private Integer userId; private Timestamp createdAt; }
  • 확인하기
    • notion image
 

4. 더미 데이터 추가하기

  • data.sql에서 추가하기
insert into user_tb(username, password, email, created_at) values('ssar', '1234', 'ssar@nate.com', now()); insert into user_tb(username, password, email, created_at) values('cos', '1234', 'cos@nate.com', now()); insert into board_tb(title, content, user_id, created_at) values('제목1', '내용1', 1, now()); insert into board_tb(title, content, user_id, created_at) values('제목2', '내용2', 1, now()); insert into board_tb(title, content, user_id, created_at) values('제목3', '내용3', 1, now()); insert into board_tb(title, content, user_id, created_at) values('제목4', '내용4', 2, now()); insert into board_tb(title, content, user_id, created_at) values('title5', '내용5', 2, now()); insert into board_tb(title, content, user_id, created_at) values('title6', '내용6', 2, now()); insert into board_tb(title, content, user_id, created_at) values('title7', '내용7', 2, now()); insert into board_tb(title, content, user_id, created_at) values('title8', '내용8', 2, now()); insert into board_tb(title, content, user_id, created_at) values('title9', '내용9', 2, now()); insert into reply_tb(comment, board_id, user_id, created_at) values('댓글1', 1, 1, now()); insert into reply_tb(comment, board_id, user_id, created_at) values('댓글2', 4, 1, now()); insert into reply_tb(comment, board_id, user_id, created_at) values('댓글3', 4, 1, now()); insert into reply_tb(comment, board_id, user_id, created_at) values('댓글4', 4, 2, now()); insert into love_tb(board_id, user_id, created_at) values(9, 1, now()); insert into love_tb(board_id, user_id, created_at) values(9, 2, now()); insert into love_tb(board_id, user_id, created_at) values(8, 2, now());
notion image
 

5. 좋아요 로직 구현하기

  • isLove = true일 때 → fas heart와 빨간색으로 바뀌게 하기
  • isLove = false일 때 → 좋아요 안 누른 상태
{{> 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> <i class="far fa-heart my-cursor"></i> <span>10</span> <i class="fas fa-heart my-cursor text-danger"></i> <span>5</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> {{> layout/footer}}
notion image
 
  • 좋아요 부분 머스태치 분법으로 설정하기
{{> 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> {{#isLove}} <i class="fas fa-heart my-cursor text-danger"></i> <span>5</span> {{/isLove}} {{^isLove}} <i class="far fa-heart my-cursor"></i> <span>10</span> {{/isLove}} </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> {{> layout/footer}}
notion image
 

6. /board/{id}에서 설정하기

package shop.mtcoding.blog.board; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpSession; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.*; import shop.mtcoding.blog.reply.ReplyRepository; import shop.mtcoding.blog.user.User; import java.util.List; @RequiredArgsConstructor @Controller public class BoardController { private final HttpSession session; // DI private final BoardRepository boardRepository; // DI private final ReplyRepository replyRepository; // ?title=제목1&content=내용1 // title=제목1&content=내용1 // 쿼리 스트링과 -x-www-form-urlencoded와 파싱 방법이 동일함 @PostMapping("/board/{id}/update") public String update(@PathVariable int id, BoardRequest.UpdateDTO requestDTO) { // 파싱 전략이 json으로 바뀜 // System.out.println(requestDTO); 정보 받기 확인 // 인증 체크하기 User sessionUser = (User) session.getAttribute("sessionUser"); if (sessionUser == null) { return "redirect:/loginForm"; } // 권한 체크하기 Board board = boardRepository.findById(id); if (board.getUserId() != sessionUser.getId()) { return "error/403"; } // update board_tb set title =?, content =?, where id =? boardRepository.update(requestDTO, id); return "redirect:/board/" + id; // 수정한 게시글로 돌아가기 } @GetMapping("/board/{id}/updateForm") // 보드에 해당 페이지 public String updateFormn(@PathVariable int id, HttpServletRequest request) { // 인증 체크하기 User sessionUser = (User) session.getAttribute("sessionUser"); if (sessionUser == null) { return "redirect:/loginForm"; } // 권한 체크하기 Board board = boardRepository.findById(id); if (board.getUserId() != sessionUser.getId()) { request.setAttribute("status", 403); request.setAttribute("msg", "게시글을 수정할 권한이 없습니다"); return "error/40x"; // 리다이렉트 하면 데이터 사라지니까 하면 안됨 } // 가방에 담기 request.setAttribute("board", board); return "board/updateForm"; } @PostMapping("/board/{id}/delete") // body데이터가 없어서 유효성 검사 안해도 됨 public String delete(@PathVariable int id) { // 1. 인증 검사하기 User sessionUser = (User) session.getAttribute("sessionUser"); if (sessionUser == null) { return "redirect:/loginForm"; } // 2. 권한 검사하기 Board board = boardRepository.findById(id); if (board.getUserId() != sessionUser.getId()) { return "error/403"; } boardRepository.deleteById(id); return "redirect:/"; } @PostMapping("/board/save") public String save(BoardRequest.SaveDTO requestDTO, HttpServletRequest request) { // 1. 인증 체크 User sessionUser = (User) session.getAttribute("sessionUser"); System.out.println("sessionUser:" + sessionUser); if (sessionUser == null) { return "redirect:/loginForm"; } // 2. 바디 데이터 확인 및 유효성 검사 System.out.println(requestDTO); if (requestDTO.getTitle().length() > 30) { request.setAttribute("status", 400); request.setAttribute("msg", "title의 길이가 30자를 초과해서는 안되요"); return "error/40x"; // BadRequest } // 3. 모델 위임 // insert into board_tb(title, content, user_id, created_at) values(?,?,?, now()); boardRepository.save(requestDTO, sessionUser.getId()); return "redirect:/"; } // http://localhost:8080?page=0 쿼리스트링 // 스프링에서 쿼리 스트링 받는 방법은 매개 변수에 받기 @GetMapping({"/"}) public String index(HttpServletRequest request, @RequestParam(defaultValue = "0") Integer page, @RequestParam(defaultValue = "") String keyword) { // isEmpty : 공백, null // isBlank : 공백, null, 화이트 space(눈에 안보이는 공백) if (keyword.isBlank()) { List<Board> boardList = boardRepository.findAll(page); // 전체 페이지 개수 int count = boardRepository.count().intValue(); int namerge = count % 3 == 0 ? 0 : 1; int allPageCount = count / 3 + namerge; request.setAttribute("boardList", boardList); request.setAttribute("first", page == 0); request.setAttribute("last", allPageCount == page + 1); request.setAttribute("prev", page - 1); request.setAttribute("next", page + 1); request.setAttribute("keyword", ""); } else { List<Board> boardList = boardRepository.findAll(page, keyword); // 전체 페이지 개수 int count = boardRepository.count(keyword).intValue(); int namerge = count % 3 == 0 ? 0 : 1; int allPageCount = count / 3 + namerge; request.setAttribute("boardList", boardList); request.setAttribute("first", page == 0); request.setAttribute("last", allPageCount == page + 1); request.setAttribute("prev", page - 1); request.setAttribute("next", page + 1); request.setAttribute("keyword", keyword); } return "index"; } @GetMapping("/board/saveForm") // /board/saveForm Get요청이 옴 public String saveForm() { // session 영역에 접근하기 위한 // 1. session 영역에 sessionUser 키 값에 user 객체가 있는지 체크하기 User sessionUser = (User) session.getAttribute("sessionUser"); // 2. 값이 null이면 로그인 페이지로 리다이렉션 if (sessionUser == null) { return "redirect:/loginForm"; } // 3. null이 아니면 /board/saveForm으로 이동 return "board/saveForm"; } // 상세보기시 호출 @GetMapping("/board/{id}") // 1이 프라이머리키 -> 뭐든 넣어도 실행시키려면 변수화시켜서 {} public String detail(@PathVariable int id, HttpServletRequest request) { User sessionUser = (User) session.getAttribute("sessionUser"); // 페이지 권한 BoardResponse.DetailDTO boardDTO = boardRepository.findByIdWithUser(id); //메서드 이름 변경 boardDTO.isBoardOwner(sessionUser); // null이면 터짐 List<BoardResponse.ReplyDTO> replyDTOList = replyRepository.findByBoardId(id, sessionUser); request.setAttribute("board", boardDTO); request.setAttribute("replyList", replyDTOList); request.setAttribute("isLove", true); request.setAttribute("loveCount", 2); return "board/detail"; } }
notion image
 

7. BoardRepository에서 save(), deleteById() 외 다 지우기

package shop.mtcoding.blog.board; import jakarta.persistence.EntityManager; import jakarta.persistence.Query; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Repository; import org.springframework.transaction.annotation.Transactional; import java.util.List; @RequiredArgsConstructor @Repository public class BoardRepository { private final EntityManager em; @Transactional public void save(BoardRequest.SaveDTO requestDTO, int userId) { Query query = em.createNativeQuery("insert into board_tb(title, content, user_id, created_at) values(?,?,?, now())"); query.setParameter(1, requestDTO.getTitle()); query.setParameter(2, requestDTO.getContent()); query.setParameter(3, userId); query.executeUpdate(); } @Transactional public void deleteById(int id) { Query query = em.createNativeQuery("delete from board_tb where id = ?"); query.setParameter(1, id); query.executeUpdate(); } }
 

8. H2에서 쿼리 테스트하기

SELECT * FROM LOVE_TB; select count(*) from love_tb where board_id=9; select *from love_tb where board_id=9; select id, user_id from love_tb where board_id=9 and user_id=1; select id, user_id from love_tb where board_id=8 and user_id=1; select user_id from love_tb where board_id=8 and user_id=1;
notion image
  • 서브 쿼리 사용하기
select id, user_id, (select count(*) from love_tb where board_id=9) loveCount from love_tb where board_id=9 and user_id=1;
notion image
  • isLove를 true/false로 나타내기
SELECT id, CASE WHEN user_id IS NULL THEN FALSE ELSE TRUE END AS isLove, (SELECT COUNT(*) FROM love_tb WHERE board_id = 9) AS loveCount FROM love_tb WHERE board_id = 9 AND user_id = 1; SELECT id, CASE WHEN user_id IS NULL THEN FALSE ELSE TRUE END AS isLove, (SELECT COUNT(*) FROM love_tb WHERE board_id = 9) AS loveCount FROM love_tb WHERE board_id = 9 AND user_id = 2; SELECT id, CASE WHEN user_id IS NULL THEN FALSE ELSE TRUE END AS isLove, (SELECT COUNT(*) FROM love_tb WHERE board_id = 9) AS loveCount FROM love_tb WHERE board_id = 9 AND user_id = 3;
notion image
 

9. entuty를 받을 수 있는 DTO 만들기

package shop.mtcoding.blog.love; import lombok.AllArgsConstructor; import lombok.Data; public class LoveResponse { @AllArgsConstructor @Data public static class DetailDTO { private Integer id; private Boolean isLove; private Long loveCount; } }
 
  • 결과는 무조건 1개
  • 복합키 걸어야 함
좋아요는 하나의 게시글에 두 번 못함
@Table(name ="", uniqueConstraints = { // 복합키 만들기 @UniqueConstraint( name="NAME_AGE_UNIQUE", columnNames={"NAME","AGE"} )})
package shop.mtcoding.blog.love; import jakarta.persistence.*; import lombok.Data; import java.sql.Timestamp; @Table(name = "love_tb", uniqueConstraints = { // 복합키 만들기 @UniqueConstraint( name = "love_uk", columnNames = {"board_id", "user_id"} // 컬럼명 적어주기 )}) @Data @Entity // 테이블 생성하기 위해 필요한 어노테이션 public class Love { @Id // PK 설정 @GeneratedValue(strategy = GenerationType.IDENTITY) // auto_increment 전략 private Integer id; private Integer boardId; private Integer userId; private Timestamp createdAt; }
  • 확인하기
notion image
notion image
 

10. LoveRepository에 findLove() 만들기

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 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); Object[] row = (Object[]) query.getSingleResult(); Integer id = (Integer) row[0]; Boolean isLove = (Boolean) row[1]; Long loveCount = (Long) row[2]; 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; } }
  • test하기
package shop.mtcoding.blog.love; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; import org.springframework.context.annotation.Import; @Import(LoveRepository.class) @DataJpaTest public class LoveRepositoryTest { @Autowired private LoveRepository loveRepository; @Test public void findLove_test() { // given int boardId = 9; int sessionUserId = 1; // when loveRepository.findLove(boardId, sessionUserId); } }
 
package shop.mtcoding.blog.board; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpSession; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.*; import shop.mtcoding.blog.love.LoveRepository; import shop.mtcoding.blog.love.LoveResponse; import shop.mtcoding.blog.reply.ReplyRepository; import shop.mtcoding.blog.user.User; import java.util.List; @RequiredArgsConstructor @Controller public class BoardController { private final HttpSession session; // DI private final BoardRepository boardRepository; // DI private final ReplyRepository replyRepository; private final LoveRepository loveRepository; // ?title=제목1&content=내용1 // title=제목1&content=내용1 // 쿼리 스트링과 -x-www-form-urlencoded와 파싱 방법이 동일함 @PostMapping("/board/{id}/update") public String update(@PathVariable int id, BoardRequest.UpdateDTO requestDTO) { // 파싱 전략이 json으로 바뀜 // System.out.println(requestDTO); 정보 받기 확인 // 인증 체크하기 User sessionUser = (User) session.getAttribute("sessionUser"); if (sessionUser == null) { return "redirect:/loginForm"; } // 권한 체크하기 Board board = boardRepository.findById(id); if (board.getUserId() != sessionUser.getId()) { return "error/403"; } // update board_tb set title =?, content =?, where id =? boardRepository.update(requestDTO, id); return "redirect:/board/" + id; // 수정한 게시글로 돌아가기 } @GetMapping("/board/{id}/updateForm") // 보드에 해당 페이지 public String updateFormn(@PathVariable int id, HttpServletRequest request) { // 인증 체크하기 User sessionUser = (User) session.getAttribute("sessionUser"); if (sessionUser == null) { return "redirect:/loginForm"; } // 권한 체크하기 Board board = boardRepository.findById(id); if (board.getUserId() != sessionUser.getId()) { request.setAttribute("status", 403); request.setAttribute("msg", "게시글을 수정할 권한이 없습니다"); return "error/40x"; // 리다이렉트 하면 데이터 사라지니까 하면 안됨 } // 가방에 담기 request.setAttribute("board", board); return "board/updateForm"; } @PostMapping("/board/{id}/delete") // body데이터가 없어서 유효성 검사 안해도 됨 public String delete(@PathVariable int id) { // 1. 인증 검사하기 User sessionUser = (User) session.getAttribute("sessionUser"); if (sessionUser == null) { return "redirect:/loginForm"; } // 2. 권한 검사하기 Board board = boardRepository.findById(id); if (board.getUserId() != sessionUser.getId()) { return "error/403"; } boardRepository.deleteById(id); return "redirect:/"; } @PostMapping("/board/save") public String save(BoardRequest.SaveDTO requestDTO, HttpServletRequest request) { // 1. 인증 체크 User sessionUser = (User) session.getAttribute("sessionUser"); System.out.println("sessionUser:" + sessionUser); if (sessionUser == null) { return "redirect:/loginForm"; } // 2. 바디 데이터 확인 및 유효성 검사 System.out.println(requestDTO); if (requestDTO.getTitle().length() > 30) { request.setAttribute("status", 400); request.setAttribute("msg", "title의 길이가 30자를 초과해서는 안되요"); return "error/40x"; // BadRequest } // 3. 모델 위임 // insert into board_tb(title, content, user_id, created_at) values(?,?,?, now()); boardRepository.save(requestDTO, sessionUser.getId()); return "redirect:/"; } // http://localhost:8080?page=0 쿼리스트링 // 스프링에서 쿼리 스트링 받는 방법은 매개 변수에 받기 @GetMapping({"/"}) public String index(HttpServletRequest request, @RequestParam(defaultValue = "0") Integer page, @RequestParam(defaultValue = "") String keyword) { // isEmpty : 공백, null // isBlank : 공백, null, 화이트 space(눈에 안보이는 공백) if (keyword.isBlank()) { List<Board> boardList = boardRepository.findAll(page); // 전체 페이지 개수 int count = boardRepository.count().intValue(); int namerge = count % 3 == 0 ? 0 : 1; int allPageCount = count / 3 + namerge; request.setAttribute("boardList", boardList); request.setAttribute("first", page == 0); request.setAttribute("last", allPageCount == page + 1); request.setAttribute("prev", page - 1); request.setAttribute("next", page + 1); request.setAttribute("keyword", ""); } else { List<Board> boardList = boardRepository.findAll(page, keyword); // 전체 페이지 개수 int count = boardRepository.count(keyword).intValue(); int namerge = count % 3 == 0 ? 0 : 1; int allPageCount = count / 3 + namerge; request.setAttribute("boardList", boardList); request.setAttribute("first", page == 0); request.setAttribute("last", allPageCount == page + 1); request.setAttribute("prev", page - 1); request.setAttribute("next", page + 1); request.setAttribute("keyword", keyword); } return "index"; } @GetMapping("/board/saveForm") // /board/saveForm Get요청이 옴 public String saveForm() { // session 영역에 접근하기 위한 // 1. session 영역에 sessionUser 키 값에 user 객체가 있는지 체크하기 User sessionUser = (User) session.getAttribute("sessionUser"); // 2. 값이 null이면 로그인 페이지로 리다이렉션 if (sessionUser == null) { return "redirect:/loginForm"; } // 3. null이 아니면 /board/saveForm으로 이동 return "board/saveForm"; } // 상세보기시 호출 @GetMapping("/board/{id}") public String detail(@PathVariable int id, HttpServletRequest request) { User sessionUser = (User) session.getAttribute("sessionUser"); BoardResponse.DetailDTO boardDTO = boardRepository.findByIdWithUser(id); boardDTO.isBoardOwner(sessionUser); List<BoardResponse.ReplyDTO> replyDTOList = replyRepository.findByBoardId(id, sessionUser); request.setAttribute("board", boardDTO); request.setAttribute("replyList", replyDTOList); LoveResponse.DetailDTO loveDetailDTO = loveRepository.findLove(id, sessionUser.getId()); request.setAttribute("love", loveDetailDTO); // fas fa-heart text-danger // far fa-heart // request.setAttribute("css", "far fa-heart"); return "board/detail"; } }
 

11. 선택하기 → ajax

블로그에서 저장을 누르면 임시 저장이 되어 저장된 숫자가 늘어나는 것
  • 버튼을 클릭하는 것 : ajax
부분 리로딩이 됨
비동기 통신
  • 숫자가 바뀌는 것 : csr
  • ssr : 리다이렉션, 파일 응답
  • csr : 데이터 응답, html을 받으면 전체 새로 고침됨 → 주소가 바뀜
  • ajax를 써야하는 경우 : 인증→ 주소가 바뀌지 않음
id를 찾아야되서 어려움

12. 오류 잡기

  • 로그인 안하고 게시글 보기
package shop.mtcoding.blog.board; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpSession; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.*; import shop.mtcoding.blog.love.LoveRepository; import shop.mtcoding.blog.love.LoveResponse; import shop.mtcoding.blog.reply.ReplyRepository; import shop.mtcoding.blog.user.User; import java.util.List; @RequiredArgsConstructor @Controller public class BoardController { private final HttpSession session; // DI private final BoardRepository boardRepository; // DI private final ReplyRepository replyRepository; private final LoveRepository loveRepository; // ?title=제목1&content=내용1 // title=제목1&content=내용1 // 쿼리 스트링과 -x-www-form-urlencoded와 파싱 방법이 동일함 @PostMapping("/board/{id}/update") public String update(@PathVariable int id, BoardRequest.UpdateDTO requestDTO) { // 파싱 전략이 json으로 바뀜 // System.out.println(requestDTO); 정보 받기 확인 // 인증 체크하기 User sessionUser = (User) session.getAttribute("sessionUser"); if (sessionUser == null) { return "redirect:/loginForm"; } // 권한 체크하기 Board board = boardRepository.findById(id); if (board.getUserId() != sessionUser.getId()) { return "error/403"; } // update board_tb set title =?, content =?, where id =? boardRepository.update(requestDTO, id); return "redirect:/board/" + id; // 수정한 게시글로 돌아가기 } @GetMapping("/board/{id}/updateForm") // 보드에 해당 페이지 public String updateFormn(@PathVariable int id, HttpServletRequest request) { // 인증 체크하기 User sessionUser = (User) session.getAttribute("sessionUser"); if (sessionUser == null) { return "redirect:/loginForm"; } // 권한 체크하기 Board board = boardRepository.findById(id); if (board.getUserId() != sessionUser.getId()) { request.setAttribute("status", 403); request.setAttribute("msg", "게시글을 수정할 권한이 없습니다"); return "error/40x"; // 리다이렉트 하면 데이터 사라지니까 하면 안됨 } // 가방에 담기 request.setAttribute("board", board); return "board/updateForm"; } @PostMapping("/board/{id}/delete") // body데이터가 없어서 유효성 검사 안해도 됨 public String delete(@PathVariable int id) { // 1. 인증 검사하기 User sessionUser = (User) session.getAttribute("sessionUser"); if (sessionUser == null) { return "redirect:/loginForm"; } // 2. 권한 검사하기 Board board = boardRepository.findById(id); if (board.getUserId() != sessionUser.getId()) { return "error/403"; } boardRepository.deleteById(id); return "redirect:/"; } @PostMapping("/board/save") public String save(BoardRequest.SaveDTO requestDTO, HttpServletRequest request) { // 1. 인증 체크 User sessionUser = (User) session.getAttribute("sessionUser"); System.out.println("sessionUser:" + sessionUser); if (sessionUser == null) { return "redirect:/loginForm"; } // 2. 바디 데이터 확인 및 유효성 검사 System.out.println(requestDTO); if (requestDTO.getTitle().length() > 30) { request.setAttribute("status", 400); request.setAttribute("msg", "title의 길이가 30자를 초과해서는 안되요"); return "error/40x"; // BadRequest } // 3. 모델 위임 // insert into board_tb(title, content, user_id, created_at) values(?,?,?, now()); boardRepository.save(requestDTO, sessionUser.getId()); return "redirect:/"; } // http://localhost:8080?page=0 쿼리스트링 // 스프링에서 쿼리 스트링 받는 방법은 매개 변수에 받기 @GetMapping({"/"}) public String index(HttpServletRequest request, @RequestParam(defaultValue = "0") Integer page, @RequestParam(defaultValue = "") String keyword) { // isEmpty : 공백, null // isBlank : 공백, null, 화이트 space(눈에 안보이는 공백) if (keyword.isBlank()) { List<Board> boardList = boardRepository.findAll(page); // 전체 페이지 개수 int count = boardRepository.count().intValue(); int namerge = count % 3 == 0 ? 0 : 1; int allPageCount = count / 3 + namerge; request.setAttribute("boardList", boardList); request.setAttribute("first", page == 0); request.setAttribute("last", allPageCount == page + 1); request.setAttribute("prev", page - 1); request.setAttribute("next", page + 1); request.setAttribute("keyword", ""); } else { List<Board> boardList = boardRepository.findAll(page, keyword); // 전체 페이지 개수 int count = boardRepository.count(keyword).intValue(); int namerge = count % 3 == 0 ? 0 : 1; int allPageCount = count / 3 + namerge; request.setAttribute("boardList", boardList); request.setAttribute("first", page == 0); request.setAttribute("last", allPageCount == page + 1); request.setAttribute("prev", page - 1); request.setAttribute("next", page + 1); request.setAttribute("keyword", keyword); } return "index"; } @GetMapping("/board/saveForm") // /board/saveForm Get요청이 옴 public String saveForm() { // session 영역에 접근하기 위한 // 1. session 영역에 sessionUser 키 값에 user 객체가 있는지 체크하기 User sessionUser = (User) session.getAttribute("sessionUser"); // 2. 값이 null이면 로그인 페이지로 리다이렉션 if (sessionUser == null) { return "redirect:/loginForm"; } // 3. null이 아니면 /board/saveForm으로 이동 return "board/saveForm"; } // 상세보기시 호출 @GetMapping("/board/{id}") public String detail(@PathVariable int id, HttpServletRequest request) { User sessionUser = (User) session.getAttribute("sessionUser"); BoardResponse.DetailDTO boardDTO = boardRepository.findByIdWithUser(id); boardDTO.isBoardOwner(sessionUser); List<BoardResponse.ReplyDTO> replyDTOList = replyRepository.findByBoardId(id, sessionUser); request.setAttribute("board", boardDTO); request.setAttribute("replyList", replyDTOList); if(sessionUser == null) { LoveResponse.DetailDTO loveDetailDTO = loveRepository.findLove(id, 0); request.setAttribute("love", loveDetailDTO); }else { } // fas fa-heart text-danger // far fa-heart // request.setAttribute("css", "far fa-heart"); return "board/detail"; } }
notion image
  • 아무것도 안나오는 흰 화면
  • 오버로딩하기
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 LoveResponse.DetailDTO findLove(int boardId){ // 오버로딩하기 String q = """ SELECT id, CASE false isLove, (SELECT COUNT(*) FROM love_tb WHERE board_id = ?) AS loveCount FROM love_tb WHERE board_id = ?; """; Query query = em.createNativeQuery(q); query.setParameter(1, boardId); query.setParameter(2, boardId); Object[] row = (Object[]) query.getSingleResult(); Integer id = (Integer) row[0]; Boolean isLove = (Boolean) row[1]; Long loveCount = (Long) row[2]; 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); Object[] row = (Object[]) query.getSingleResult(); Integer id = (Integer) row[0]; Boolean isLove = (Boolean) row[1]; Long loveCount = (Long) row[2]; 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; } }
 
select 0, count(id) loveCount, false isLove FROM love_tb WHERE board_id = 9 group by board_id;
notion image
  • DB에서 안받고 강제로 넣어주기
select count(id) loveCount, false isLove FROM love_tb WHERE board_id = 9;
notion image
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 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; } }
 
Share article

Coding_study