[코드로 배우는 스프링부트 웹 프로젝트] - 프로젝트 구조 만들기(6) - 수정, 삭제, 검색

Jongwon·2022년 12월 31일
0

이전 글에서 조회까지 만든 후, 조회 창에서 수정 창으로 갈 수 있는 버튼을 만들어두었습니다. 이번 글에서는 수정과 삭제 페이지를 만들고자 합니다.

조회 페이지와 양식과 필요한 데이터는 거의 동일하기 때문에 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";
    }
profile
Backend Engineer

0개의 댓글