이전 글에서 조회까지 만든 후, 조회 창에서 수정 창으로 갈 수 있는 버튼을 만들어두었습니다. 이번 글에서는 수정과 삭제 페이지를 만들고자 합니다.
조회 페이지와 양식과 필요한 데이터는 거의 동일하기 때문에 GetMapping 컨트롤러는 동일하게 사용합니다.
@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
을 생성합니다.
그리고 아래와 같이 일부분을 변경해줍니다.
modify.html
...
<h1 class="mt-4">GuestBook Modify Page</h1>
...
//form으로 묶어주기
<form action="/guestbook/modify" method="post">
//page번호 반환하기 위해 보여지지 않는 form input값 생성
<input type="hidden" name="page" th:value="${requestDTO.page}">
<div class="form-group">
<label>Gno</label>
<input type="text" class="form-control" name="gno" th:value="${dto.gno}" readonly>
</div>
//수정가능이므로 readonly 제거
<div class="form-group">
<label>Title</label>
<input type="text" class="form-control" name="title" th:value="${dto.title}">
</div>
//수정가능이므로 readonly 제거
<div class="form-group">
<label>Content</label>
<textarea class="form-control" name="content" rows="5">[[${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>
//regDate와 modDate는 자동 입력이므로 name값을 빼고 전달해야함
<div class="form-group">
<label>RegDate</label>
<input type="text" class="form-control" th:value="${#temporals.format(dto.regDate, 'yyyy/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>
<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");
$(".removeBtn").click(function(){
actionForm
.attr("action", "/guestbook/remove")
.attr("method","post");
actionForm.submit();
});
$(".modifyBtn").click(function() {
if(!confirm("수정하시겠습니까?")) {
return;
}
actionForm
.attr("action", "/guestbook/modify")
.attr("method", "post")
.submit();
});
</script>
</th:block>
script에서는 Remove버튼이 눌렸을 때 Form의 action속성과 method속성을 조정하는 방식을 통해 Post가 modify가 아닌 remove로 가도록 설정합니다.
Modify버튼을 눌렀을 때는 "수정하시겠습니까?"라는 알림창을 띄운 후 Form이 submit을 하도록 합니다.
Form은 method 방식을 GET과 POST만 지원하기 때문에 DeleteMapping을 사용할 수 없습니다.
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);
}
}
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";
}
@PostMapping("/modify")
public String modify(GuestbookDTO dto, @ModelAttribute("requestDTO") PageRequestDTO requestDTO, RedirectAttributes redirectAttributes) {
log.info("post modify..............");
log.info("dto: " + dto);
service.modify(dto);
redirectAttributes.addAttribute("page", requestDTO.getPage());
redirectAttributes.addAttribute("gno", dto.getGno());
return "redirect:/guestbook/read";
}
다음으로는 검색을 구현하겠습니다. 검색은 앞서 Querydsl 테스트에서 진행했던 내용과 유사하게, keyword가 한 개 또는 여러개 일 때 모두 검색이 가능해야합니다.
PageRequestDTO에 검색 타입과 키워드 변수를 추가합니다.
PageRequestDTO
private String type;
private String keyword;
다음으로는 Querydsl처리를 해야하는데, 이를 인터페이스가 아닌 ServiceImpl 내부의 함수로 구현하겠습니다.
GuestbookServiceImpl
//추가
private BooleanBuilder getSearch(PageRequestDTO requestDTO) {
String type = requestDTO.getType();
BooleanBuilder booleanBuilder = new BooleanBuilder();
QGuestbook qGuestbook = QGuestbook.guestbook;
String keyword = requestDTO.getKeyword();
BooleanExpression expression = qGuestbook.gno.gt(0L);
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;
}
PageRequestDTO에서 타입은 각각 t-title, c-contents, w-writer를 의미하는 문자열입니다. 예를들어 type = tc
라면, title이나 contents 중 keyword에 해당하는 엔티티를 검색한다는 의미입니다.
getList() 메서드도 getSearch를 통해 나온 검색 조건을 이용하여 레포지토리에 접근하도록 수정합니다.
@Override
public PageResultDTO<GuestbookDTO, Guestbook> getList(PageRequestDTO requestDTO) {
Pageable pageable = requestDTO.getPageable(Sort.by("gno").descending());
BooleanBuilder booleanBuilder = getSearch(requestDTO);
Page<Guestbook> result = repository.findAll(booleanBuilder, pageable);
Function<Guestbook, GuestbookDTO> fn = (entity -> entityToDto(entity));
return new PageResultDTO<>(result, fn);
}
실제로 동작하는지 테스트 해보겠습니다.
GuestbookServiceTests
@Test
public void testSearch() {
PageRequestDTO pageRequestDTO = PageRequestDTO.builder()
.page(1)
.size(10)
.type("tc")
.keyword("3")
.build();
PageResultDTO<GuestbookDTO, Guestbook> resultDTO = service.getList(pageRequestDTO);
System.out.println("PREV: " + resultDTO.isPrev());
System.out.println("NEXT: " + resultDTO.isNext());
System.out.println("TOTAL: " + resultDTO.getTotalPage());
System.out.println("-------------------------");
for (GuestbookDTO guestbookDTO : resultDTO.getDtoList()) {
System.out.println(guestbookDTO);
}
System.out.println("========================");
resultDTO.getPageList().forEach(i -> System.out.println(i));
}
먼저 쿼리문을 보면 title이나 content에서 문자열이 있는지 where절에서 탐색 후 dto값을 반환합니다.
중간에 등록 확인을 위해 별개의 레코드들을 입력해서 결과는 다를 수 있지만 제목이나 컨텐츠에 "3"이 들어간 레코드들이 DTO에 담겨 총 6페이지가 나온 것을 확인할 수 있습니다.
이제 검색창 UI를 생성해보겠습니다.
...
//추가
<form action="/guestbook/list" method="get" id="searchForm">
<div class="input-group">
<input type="hidden" name="page" value="1">
<div class="input-group-prepend">
<select class="custom-select" name="type">
<option th:selected="${pageRequestDTO.type == null}">------</option>
<option value="t" th:selected="${pageRequestDTO.type == 't'}">제목</option>
<option value="c" th:selected="${pageRequestDTO.type == 'c'}">내용</option>
<option value="w" th:selected="${pageRequestDTO.type == 'w'}">작성자</option>
<option value="tc" th:selected="${pageRequestDTO.type == 'tc'}">제목 + 내용</option>
<option value="tcw" th:selected="${pageRequestDTO.type == 'tcw'}">제목 + 내용 + 작성자</option>
</select>
</div>
<input class="form-control" name="keyword" th:value="${pageRequestDTO.keyword}">
<div class="input-group-append" id="button-addon4">
<button class="btn btn-outline-secondary btn-search" type="button">Search</button>
<button class="btn btn-outline-secondary btn-clear" type="button">Clear</button>
</div>
</div>
</form>
<table class="table table-striped">
<thead>
...
<tbody>
<tr th:each="dto : ${result.dtoList}">
<th scope="row">
//수정
<a th:href="@{/guestbook/read(gno=${dto.gno}, page=${result.page}, type=${pageRequestDTO.type}, keyword=${pageRequestDTO.keyword})}">
[[${dto.gno}]]
</a>
...
<ul class="pagination h-100 justify-content-center align-items-center">
<li class="page-item" th:if="${result.prev}">
//수정
<a class="page-link" th:href="@{/guestbook/list(page=${result.start - 1}, type=${pageRequestDTO.type}, keyword=${pageRequestDTO.keyword})}" tabindex="-1">Previous</a>
</li>
<li th:class=" 'page-item ' + ${result.page == page ? 'active' : ''} " th:each="page: ${result.pageList}">
//수정
<a class="page-link" th:href="@{/guestbook/list(page=${page}, type=${pageRequestDTO.type}, keyword=${pageRequestDTO.keyword})}">
[[${page}]]
</a>
</li>
<li class="page-item" th:if="${result.next}">
//수정
<a class="page-link" th:href="@{/guestbook/list(page=${result.end + 1}, type=${pageRequestDTO.type}, keyword=${pageRequestDTO.keyword})}">Next</a>
</li>
</ul>
...
<script th:inline="javascript">
var msg = [[${msg}]];
console.log(msg);
const $modal = $(".modal")
if(msg) {
$('.modal').show();
}
$(".close").on("click", () => {
$modal.hide()
});
//추가
var searchForm = $("#searchForm");
$('.btn-search').click(function(e) {
searchForm.submit();
});
$('.btn-clear').click(function(e) {
searchForm.empty().submit();
});
</script>
검색 Form을 h1바로 아래 생성해준 후, script에서 Search버튼을 눌렀을 때는 Submit이 동작, Clear를 눌렀을 때는 Form을 초기화한 후 Submit하여 처음 list로 돌아오도록 구성하였습니다.
또한 검색을 한 후, 페이지를 변환할 때 링크 역시 수정해주어야 검색한 페이지를 넘길 수 있으므로 중간에 페이지를 넘기는 버튼의 링크들을 수정해줍니다.
마지막으로 검색을 한 후, 특정 번호를 클릭해서 이동했다면, 다시 검색된 리스트로 돌아갈 수 있게끔 URL에 type과 keyword를 파라미터로 넣어놨습니다.
또한 read.html
에서 목록으로 돌아가는 버튼의 링크도 변경해야합니다.
read.html
<a th:href="@{/guestbook/modify(gno=${dto.gno}, page=${requestDTO.page}, type=${requestDTO.type}, keyword=${requestDTO.keyword})}">
<button type="button" class="btn btn-primary">Modify</button>
</a>
<a th:href="@{/guestbook/list(page=${requestDTO.page}, type=${requestDTO.page}, keyword=${requestDTO.keyword})}">
<button type="button" class="btn btn-info">List</button>
</a>
mofidy.html
역시 수정 후 검색했던 목록 페이지로 돌아가야 하므로 수정이 필요합니다.
modify.html
...
<h1 class="mt-4">GuestBook Modify Page</h1>
<form action="/guestbook/modify" method="post">
<input type="hidden" name="page" th:value="${requestDTO.page}">
//추가
<input type="hidden" name="type" th:value="${requestDTO.type}">
<input type="hidden" name="keyword" th:value="${requestDTO.keyword}">
...
//script에 추가
$(".listBtn").click(function() {
var page = $("input[name='page']");
var type = $("input[name='type']");
var keyword = $("input[name='keyword']");
actionForm.empty();
actionForm.append(page);
actionForm.append(type);
actionForm.append(keyword);
actionForm
.attr("action", "/guestbook/list")
.attr("method", "get");
actionForm.submit();
})
마지막으로 수정 후에 목록으로 돌아갈 때도 검색 목록으로 갈 수 있도록 PostMapping도 변경합니다.
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);
redirectAttributes.addAttribute("page", requestDTO.getPage());
redirectAttributes.addAttribute("type", requestDTO.getType());
redirectAttributes.addAttribute("keyword", requestDTO.getKeyword());
redirectAttributes.addAttribute("gno", dto.getGno());
return "redirect:/guestbook/read";
}