Spring/코드로 배우는 스프링부트 웹 프로젝트

코드로 배우는 스프링부트 웹 프로젝트 Day 10

한 면만 쓴 종이 2022. 8. 25. 21:28

/* 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