
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;
}
- 확인하기

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());

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

- 좋아요 부분 머스태치 분법으로 설정하기
{{> 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}}

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

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;

- 서브 쿼리 사용하기
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;

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

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;
}
- 확인하기


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

- 아무것도 안나오는 흰 화면
- 오버로딩하기
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;

- DB에서 안받고 강제로 넣어주기
select count(id) loveCount, false isLove FROM love_tb WHERE board_id = 9;

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