일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | 6 | 7 |
8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 | 16 | 17 | 18 | 19 | 20 | 21 |
22 | 23 | 24 | 25 | 26 | 27 | 28 |
29 | 30 |
- MongoDB
- 스프링부트
- in-memory
- 깃허브
- 동적계획법
- JPA
- Spring Boot
- Redis
- 가상 면접 사례로 배우는 대규모 시스템 설계 기초
- 호이스팅
- 다이나믹프로그래밍
- 영속성 컨텍스트
- 이벤트루프
- 분할정복
- sqld
- 정처기
- 자바의 정석
- 캐시
- VMware
- spring security
- 정보처리기사
- 스프링 부트
- SQL
- github
- 레디스
- 스프링 시큐리티
- document database
- 게시판
- 실행 컨텍스트
- NoSQL
- Today
- Total
FreeHand
[Spring Boot] 게시판 프로젝트 - 03. 게시판 글 CRUD 본문

이전 글
[Spring Boot] 게시판 프로젝트 - 02. 회원가입 및 로그인
목차1. 회원가입 작성2. 로그인 작성 1. 회원가입 작성 [joinForm.jsp] Username: Password: Email address: 회원가입[user.js]let index = { init: function() { $("#btn-save").on("click", () => { this.save(); }); }, save: function() { let d
pressky99.tistory.com
목차
1. 글 작성
2. 글 조회
3. 글 수정
4. 글 삭제
5. 트러블슈팅
1. 글 작성

[saveForm.jsp]
... 생략 ...
<!-- Form start -->
<div class="container">
<form>
<div class="form-group">
<label for="title">Title:</label>
<input type="text" class="form-control" placeholder="Enter title" id="title">
</div>
<div class="form-group">
<label for="content">Content:</label>
<textarea class="form-control summernote" rows="5" id="content"></textarea>
</div>
</form>
<button id="btn-save" class="btn btn-primary">완료</button>
</div>
<!-- Form end -->
<div id="summernote"></div>
<script>
$('.summernote').summernote({
tabsize: 2,
height: 300
});
</script>
<script src="/js/board.js"></script>
[board.js]
let index = {
init: function() {
$("#btn-save").on("click", () => {
this.save();
});
},
save: function() {
let data = {
title: $("#title").val(),
content: $("#content").val()
}
$.ajax({
type: "POST",
url: "/api/board",
data: JSON.stringify(data),
contentType: "application/json; charset=utf-8",
dataType: "json"
}).done(function() {
alert("글쓰기 완료");
location.href = "/";
}).fail(function(error) {
alert(JSON.stringify(error));
});
}
}
index.init();
글 작성에는 summernote 라이브러리를 사용했다.
ajax로 "/api/board"에 post 요청을 보낸다.
@RequiredArgsConstructor
@RestController
public class BoardApiController {
private final BoardService boardService;
@PostMapping("/api/board")
public ResponseEntity<Integer> save(@RequestBody Board board,
@AuthenticationPrincipal PrincipalDetail principal) {
boardService.save(board, principal.getUser());
return new ResponseEntity<>(1, HttpStatus.CREATED);
}
}
@RequiredArgsConstructor
@Service
public class BoardService {
private final BoardRepository boardRepository;
@Transactional
public void save(Board board, User user) {
board.setUser(user);
boardRepository.save(board);
}
}
컨트롤러는 입력된 board와 글을 작성한 작성자의 정보(principal)를 서비스 계층에 넘긴다.
서비스에서는 해당 글과 작성자를 저장한다.
2. 글 조회


[index.jsp]
... 생략 ...
<div class="container">
<!-- Card start -->
<c:forEach var="board" items="${boards.content}">
<div class="card m-2">
<div class="card-body">
<h4 class="card-title">${board.title}</h4>
<a href="/board/${board.id}" class="btn btn-primary">상세보기</a>
</div>
</div>
</c:forEach>
<!-- Card end -->
<!-- Pagination start -->
<ul class="pagination justify-content-center">
<c:choose>
<c:when test="${boards.first}">
<li class="page-item disabled"><a class="page-link" href="?page=${boards.number - 1}">Previous</a></li>
</c:when>
<c:otherwise>
<li class="page-item"><a class="page-link" href="?page=${boards.number - 1}">Previous</a></li>
</c:otherwise>
</c:choose>
<c:choose>
<c:when test="${boards.last}">
<li class="page-item disabled"><a class="page-link" href="?page=${boards.number + 1}">Next</a></li>
</c:when>
<c:otherwise>
<li class="page-item"><a class="page-link" href="?page=${boards.number + 1}">Next</a></li>
</c:otherwise>
</c:choose>
</ul>
<!-- Pagination end -->
@RequiredArgsConstructor
@Controller
public class BoardController {
private final BoardService boardService;
@GetMapping("/")
public String index(@PageableDefault(size=4, sort="createdAt", direction = Sort.Direction.DESC) Pageable pageable,
Model model) {
model.addAttribute("boards", boardService.findAll(pageable));
return "index";
}
@GetMapping("/board/saveForm")
public String saveForm() {
return "board/saveForm";
}
@GetMapping("/board/{id}")
public String findById(@PathVariable Long id, Model model) {
model.addAttribute(boardService.findById(id));
return "board/detail";
}
}
[BoardService.java]
@Transactional(readOnly = true)
public Page<Board> findAll(Pageable pageable) {
return boardRepository.findAll(pageable);
}
@Transactional(readOnly = true)
public Board findById(Long id) {
return boardRepository.findById(id)
.orElseThrow(() -> new NoSuchElementException("글 찾기 실패: 해당 글을 찾을 수 없음."));
}
작성한 글은 메인페이지에서 조회할 수 있다.
model에 최신순으로 정렬된 페이지 객체를 담아 index.jsp에서 값을 읽는다.
상세 페이지 조회는 id로 글을 조회하여 model에 담아 전달한다.
3. 글 수정

@RequiredArgsConstructor
@RestController
public class BoardApiController {
private final BoardService boardService;
// 생략
@PutMapping("/api/board/{id}")
public ResponseEntity<Integer> update(@PathVariable Long id,
@RequestBody Board board) {
boardService.update(id, board);
return new ResponseEntity<>(1, HttpStatus.OK);
}
}
@RequiredArgsConstructor
@Service
public class BoardService {
private final BoardRepository boardRepository;
// 생략
@Transactional
public void update(Long id, Board board) {
Board originBoard = boardRepository.findById(id)
.orElseThrow(() -> new NoSuchElementException("글 찾기 실패: 해당 글을 찾을 수 없음."));
originBoard.setTitle(board.getTitle());
originBoard.setContent(board.getContent());
}
}
수정 페이지의 jsp 코드는 작성 페이지와 거의 동일하다.
해당 id의 글을 조회(findById)하여 jpa 영속성 컨텍스트에 영속화시키고 setter를 통해 값을 변경하여 jpa의 더티체킹으로 쿼리가 실행된다. 더티체킹은 영속성 컨텍스트에 있는 엔티티에만 발생한다.
4. 글 삭제
@RequiredArgsConstructor
@RestController
public class BoardApiController {
private final BoardService boardService;
// 생략
@DeleteMapping("/api/board/{id}")
public ResponseEntity<Integer> delete(@PathVariable Long id,
@AuthenticationPrincipal PrincipalDetail principal) throws IllegalAccessException {
boardService.delete(id, principal);
return new ResponseEntity<>(1, HttpStatus.OK);
}
}
@RequiredArgsConstructor
@Service
public class BoardService {
private final BoardRepository boardRepository;
// 생략
@Transactional
public void delete(Long id, PrincipalDetail principal) throws IllegalAccessException {
Board board = boardRepository.findById(id)
.orElseThrow(() -> new NoSuchElementException("글 찾기 실패: 해당 글을 찾을 수 없음."));
if (board.getUser().getId() != principal.getUser().getId()) {
throw new IllegalAccessException("글 삭제 실패: 해당 글을 삭제할 권한이 없음.");
}
boardRepository.deleteById(id);
}
}
상세 페이지에서 삭제 버튼을 누르면 ajax로 DELETE 요청을 보내서 글이 삭제되도록 작성했다.
처음에는 deleteById()만 호출했는데, 문제점을 발견하고 작성자와 현재 사용자가 동일인인지 확인하는 코드를 추가했다.
그 이유는 바로 밑에 있는 트러블슈팅에 있다.
5. 트러블슈팅
글 상세 페이지에서 내가 작성한 글에만 "수정"과 "삭제" 버튼이 보이게 작성했다.
그런데 개발자도구에서 네트워크 탭을 보다가 조금 이상한 부분이 보였다.
상세 페이지가 로드될 때 board.js가 같이 로드되는데, 로드된 board.js의 함수를 보고 콘솔에서 호출하면 다른 사람의 글도 삭제할 수가 있다는 것이다.

그래서 해당 글의 작성자와 로그인 사용자 정보를 비교하여 같은 사용자이면 삭제하도록 작성했다.
생각도 못한 허점을 발견해서 다행이었고 새로운 것을 알게되어서 흥미로웠다.
다음 글
[Spring Boot] 게시판 프로젝트 - 04. 회원정보 수정
이전 글 [Spring Boot] 게시판 프로젝트 - 03. 게시판 글 CRUD이전 글 { this.save(); }); }, save: function() { let d" data-og-host="pressky99.tistory.com" data-og-source-url="https://pressky99.tistory.com/39" data-og-url="https://pressky99.ti
pressky99.tistory.com
'Web > Spring' 카테고리의 다른 글
[Spring Boot] 게시판 프로젝트 - 05. 댓글 기능 (0) | 2023.10.04 |
---|---|
[Spring Boot] 게시판 프로젝트 - 04. 회원정보 수정 (0) | 2023.10.01 |
[Spring Boot] 게시판 프로젝트 - 02. 회원가입 및 로그인 (0) | 2023.09.25 |
[Spring Boot] 게시판 프로젝트 - 01. 개발환경 및 엔티티 작성 (0) | 2023.09.25 |
[스프링 Core] 스프링 핵심 원리 - 기본편1 (0) | 2023.09.14 |