코드로 배우는 스프링부트 웹 프로젝트 Day 10
/* Annotation들은 스프링부트 프로젝트의 Annotation 정리 페이지에 따로 정리해두었습니다. */
[방명록의 수정/삭제 처리]
계획
- Guestbook의 수정은 Post 방식으로 처리하고 다시 수정된 결과를 확인할 수 있는 조회 화면으로 이동
- 삭제는 Post방식으로 처리하고 목록 화면으로 이동
- 목록을 이동하는 작업은 Get 방식으로 처리. 기존에 사용하던 페이지 번호 등을 유지해서 이동!
수정과 삭제는 모두 '수정 및 삭제가 가능한 페이지'로 이동한 상태에서 수정과 삭제 중 선택을 해서 작업이 이루어짐
이를 구현하기 위해 GuestbookController에서는 조회와 비슷하게 Get방식으로 진입하는 'gusetbook/modify'를 기존의 read()에 어노테이션의 값을 변경해서 처리
@GetMapping({"/read", "/modify"})
public void read(long gno, @ModelAttribute("requestDTO") PageRequestDTO requestDTO, Model model) {
log.info("gno: " + gno);
GuestbookDTO dto = service.read(gno);
model.addAttribute("dto" ,dto);
}
read.html을 modify.html을 만들어서 복사.붙여넣기 한다.
<h1 class="mt-4">GuestBook Modify Page</h1>
<form action="/guestbook/modify" method="post">
<h1>태그의 내용을 modify로 바꾸고, form 태그로 수정된 내용을 감싸 Post 방식으로 처리하도록 함
<div class="form-group">
<label>Gno</label>
<input type="text" class="form-control" name="gno" th:value="${dto.gno}" readonly>
</div>
<div class="form-group">
<label>Title</label>
<input type="text" class="form-control" name="title" th:value="${dto.title}">
</div>
<div class="form-group">
<label>Content</label>
<textarea class="form-control" rows="5" name="content">[[${dto.content}]]</textarea>
</div>
<div class="form-group">
<label>Writer</label>
<input type="text" class="form-control" name="writer" th:value="${dto.writer}" readonly>
</div>
<div class="form-group">
<label>RegDate</label>
<input type="text" class="form-control" th:value="${#temporals.format(dto.regDate, 'yyy/MM/dd HH:mm:ss')}" readonly>
</div>
<div class="form-group">
<label>ModDate</label>
<input type="text" class="form-control" th:value="${#temporals.format(dto.modDate, 'yyyy/MM/dd HH:mm:ss')}" readonly>
</div>
</form>
Title과 내용만 수정 가능하도록 한다. (readonly삭제)
regDate와 modDate는 수정이 불가능할 뿐더러, JPA에서 자동 처리되므로, name속성을 없앰
</form>
<button type="button" class="btn btn-primary">Modify</button>
<button type="button" class="btn btn-info">List</button>
<button type="button" class="btn btn-danger">Remove</button>
화면에 표시할 수정, List, 삭제용 버튼 추가
위와 같이 Title과 Content만 수정 가능하도록 처리된다.
수정, 삭제, 목록의 각 버튼을 클릭할 때 마다 다른 이벤트를 처리해야 한다.
이는 <form>태그의 action속성을 통해 처리 가능하다. 해당 처리는 조금 뒤쪽에 한다.
[서비스 계층에서의 수정과 삭제]
수정과 삭제 관련 기능 추가
GuestbookService 인터페이스 추가
void remove(Long gno);
void modify(GuestbookDTO dto);
GuestbookServiceImpl 클래스 추가
@Override
public void remove(Long gno) {
repository.deleteById(gno);
}
@Override
public void modify(GuestbookDTO dto) {
//업데이트 하는 항목은 '제목', '내용'
Optional<Guestbook> result = repository.findById(dto.getGno());
if(result.isPresent()) {
Guestbook entity = result.get();
entity.changeTitle(dto.getTitle());
entity.changeContent(dto.getContent());
repository.save(entity);
}
}
remove()와 modify()를 구현
[컨트롤러의 게시물 삭제]
삭제 기능은 Post 방식으로 gno값을 전달하고, 삭제 후에는 다시 목록의 첫 페이지로 이동하는 방식이 가장 보편적임
GuestbookController 클래스
@PostMapping("/remove")
public String remove(long gno, RedirectAttributes redirectAttributes) {
log.info("gno: " + gno);
service.remove(gno);
redirectAttributes.addFlashAttribute("msg", gno);
return "redirect:/guestbook/list";
}
[modify.html 삭제 처리]
삭제 작업은 Get 방식으로 수정 페이지에 들어가서 '삭제' 버튼을 클릭하는 방식으로 제작
modify.html 수정
<button type="button" class="btn btn-primary modifyBtn">Modify</button>
<button type="button" class="btn btn-info listBtn">List</button>
<button type="button" class="btn btn-danger removeBtn">Remove</button>
각 버튼 구별을 위해 수정
<script th:inline="javascript">
var actionForm = $("form"); //form 태그 객체
$(".removeBtn").click(function() {
actionForm
.attr("action", "/guestbook/remove")
.attr("method", "post");
actionForm.submit();
});
</script>
자바스크립트 인라인 기능을 사용해 구현
<form> 태그의 action 속성에 값으로 /guestbook/remove 를 지정
<form> 태그의 method 속성에 값으로 post를 지정
<form> 태그는 사용자가 입력한 정보를 서버로 전달하는 역할 ↴
<form> 태그 내에는 <input> 태그로 gno가 있기 때문에 컨트롤러에서는 여러 파라미터 중에서 gno를 추출해서 삭제 시에 이용함
[POST 방식의 수정 처리]
수정 처리는 POST 방식으로 이루어져야 함
고려해야 할 점
- 수정 시에 수정해야하는 내용(제목, 내용, 글 번호)이 전달되어야 함
- 수정 후에는 목록 페이지로 이동하거나 조회 페이지로 이동해야 함 (이때, 가능하면 기존의 페이지 번호를 유지하는 것이 좋음)
modify.html에는 /guestbook/read 로 이동할 때 페이지 번호가 파라미터로 전달되고 있고, 수정 페이지로 이동할 경우에도 같이 전달됨
이를 이용하여 수정이 완료된 후에도 동일한 목록 및 조회 페이지를 유지할 수 있도록 page값도 <form> 태그에 추가해서 전달하도록 함
modify.html
<form action="/guestbook/modify" method="post">
<!-- 페이지 번호 -->
<input type="hidden" name="page" th:value="${requestDTO.page}">
[컨트롤러의 수정 처리]
GuestbookController에서는 Guestbook 자체의 수정과 페이징 관련 데이터 처리를 같이 진행해주어야 함
GuestbookController 클래스
@PostMapping("/modify")
public String modify(GuestbookDTO dto, @ModelAttribute("requestDTO") PageRequestDTO requestDTO, RedirectAttributes redirectAttributes) {
log.info("post modify..................................................");
log.info("dto: " + dto);
service.modify(dto);
service.modify(dto);
redirectAttributes.addAttribute("page", requestDTO.getPage());
redirectAttributes.addAttribute("gno" ,dto.getGno());
return "redirect:/guestbook/read";
}
수정해야하는 글의 정보를 가지는 GuestbookDTO, 기존의 페이지 정보를 유지하기 위한 PageRequestDTO, 리다이렉트로 이동하기 위한 RedirectAttributes 세 가지가 파라미터로 필요함
수정을 완료하면, read 페이지로 이동 (기존 페이지 정보도 같이 유지해서 조회 페이지에서 다시 목록 페이지로 이동할 수 있도록 함)
[수정 화면에서의 이벤트 처리]
modify.html
$(".modifyBtn").click(function() {
if(!confirm("수정하시겠습니까?")) {
return;
}
actionForm
.attr("action", "/guestbook/modify")
.attr("method", "post")
.submit();
});
confirm을 통해 정말 수정할 것인지 재확인 후, Post 방식으로 서버에 전송
이후, 다시 조회 페이지(read)로 이동
[수정 화면에서 다시 목록 페이지로]
목록 페이지로 이동하는 버튼 처리
목록 페이지로 이동할 때에는 page 파라미터 외에는 별도로 필요하지 않음
=> page를 제외한 파라미터들을 지운 상태로 처리
$(".listBtn").click(function () {
var pageInfo = $("input[name='page']");
actionForm.empty(); //form 태그의 모든 내용을 지움
actionForm.append(pageInfo); //목록 페이지 이동에 필요한 내용을 다시 추가
actionForm
.attr("action", "/guestbook/list")
.attr("method", "get");
console.log(actionForm.html());
actionForm.submit();
});
[검색 처리]
검색 처리는 크게 서버 사이드 처리와 화면 쪽의 처리로 나눌 수 있음
서버 사이드 처리
- PageRequestDTO에 검색 타입(type)과 키워드(keyword)를 추가
- 이하 서비스 계층에서 Querydsl을 이용해서 검색 처리
검색 항목 분류
- 제목, 내용, 작성자
- 제목 or 내용
- 제목 or 내용 or 작성자
[서버측 검색 처리]
PageRequestDTO에 검색 타입과 검색 키워드 추가
PageRequestDTO 클래스
private String type;
private String keyword;
type과 keyword 추가
[서비스 계층의 검색 구현과 테스트]
동적으로 검색 조건이 처리되는 경우의 실제 코딩은 Querydsl을 통해서 BooleanBuilder를 작성
GuestbookRepository는 Querydsl로 작성된 BooleanBuilder를 findAll() 처리하는 용도로 사용
BooleanBuilder는 별도의 클래스 등을 작성하여 처리할 수 있지만, 간단히 처리하기 위해 GuestbookServiceImpl에 메서드를 작성해서 처리
GuestbookServiceImpl 클래스에 추가
private BooleanBuilder getSearch(PageRequestDTO requestDTO) { //Querydsl 처리
String type = requestDTO.getType();
BooleanBuilder booleanBuilder = new BooleanBuilder();
QGuestbook qGuestbook = QGuestbook.guestbook;
String keyword = requestDTO.getKeyword();
BooleanExpression expression = qGuestbook.gno.gt(0L); //gno > 0 조건만 생성
booleanBuilder.and(expression);
if(type == null || type.trim().length() == 0) { //검색 조건이 없는 경우
return booleanBuilder;
}
//검색 조건 작성
BooleanBuilder conditionBuilder = new BooleanBuilder();
if (type.contains("t")) {
conditionBuilder.or(qGuestbook.title.contains(keyword));
}
if (type.contains("c")) {
conditionBuilder.or(qGuestbook.content.contains(keyword));
}
if (type.contains("w")) {
conditionBuilder.or(qGuestbook.writer.contains(keyword));
}
//모든 조건 통합
booleanBuilder.and(conditionBuilder);
return booleanBuilder;
}
검색 조건이 있는 경우, conditionBuilder 변수를 통해 검색 조건들을 or로 연결하여 return
검색 조건이 없는 경우, gno > 0 으로만 return