본문 바로가기

Server Programming/Spring Boot Backend Programming

[Spring 부트 - 방명록 미니 프로젝트] 2-3. 서비스, DTO, 컨트롤러 작성 (2)

반응형

요구사항

  •  목록
    • 전체 목록 페이징 처리해 조회
    • 제목/내용/작성자 항목으로 검색과 페이징 처리
  • 등록
    • 새로운 글 등록 후 다시 목록 화면으로 이동
  • 조회
    • 목록 화면에서 특정 글 선택시 자동으로 조회 화면으로 이동
    • 수정/삭제가 가능한 화면으로 이동가능
  • 수정/삭제
    • 수정 화면에서는 삭제가 가능하고, 삭제 후에는 목록 페이지로 이동
    • 글 수정 후에는 다시 조회 화면으로 이동해 수정 내용 확인 가능

#조회

1. 서비스 구현

: 인터페이스에 추상메서드 추가하고 ,구현 클래스에서 구현

public interface GuestbookService {

GuestbookDTO read(Long gno);

}
@Service
@Log4j2
//이 어노테이션은 초기화 되지않은 final 필드나, @NonNull 이 붙은 필드에 대해 생성자를 생성
@RequiredArgsConstructor
public class GuestbookServiceImpl implements GuestbookService{

    @Autowired
    private final GuestbookRepository repository;
    //리포지토리는 반드시 private final로 선언한다.

@Override
    public GuestbookDTO read(Long gno){

        //Null 값과 널이 아닌 값 모두 담을 수 있기 때문에 사용
        Optional<Guestbook> result = repository.findById(gno);

        //널 체크 후 널 아니면 DTO로 변환해서 전달, null이면 null을 전달
        return result.isPresent()? entityToDto(result.get()):null;
    }
}

 

2. 컨트롤러 작성

 

3. 조회 페이지 작성

: readonly 속성을 적용하고, 목록페이지로 이동, 수정과 삭제 링크

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">

<th:block th:replace="~{/layout/basic :: setContent(~{this::content} )}">

    <th:block th:fragment="content">

        <h1 class="mt-4">GuestBook Read Page</h1>

        <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}" readonly >
        </div>
        <div class="form-group">
            <label >Content</label>
            <textarea class="form-control" rows="5" name="content" readonly>[[${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" name="regDate" 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" name="modDate" th:value="${#temporals.format(dto.modDate, 'yyyy/MM/dd HH:mm:ss')}" readonly>
        </div>

<!--        <a th:href="@{/guestbook/modify(gno = ${dto.gno}, page=${requestDTO.page})}"><button type="button" class="btn btn-primary">Modify</button></a>-->

<!--        <a th:href="@{/guestbook/list(page=${requestDTO.page})}"><button type="button" class="btn btn-info">List</button></a>-->

        <!-- type과 keyword 처리-->
        <!-- , type=${requestDTO.type}, keyword =${requestDTO.keyword})-->

        <a th:href="@{/guestbook/modify(gno = ${dto.gno}, page=${requestDTO.page})}">
            <button type="button" class="btn btn-primary">Modify</button>
        </a>

        <a th:href="@{/guestbook/list(page=${requestDTO.page} )}">
            <button type="button" class="btn btn-info">List</button>
        </a>

    </th:block>

</th:block>

(1) 컨트롤러에서 (서비스로) 전달한  dto라는 이름의 DTO를 이용해 글의 내용을 출력

(2) @ModelAttribute로 처리된 requestDTO로 페이지 관련 부분을 처리

 


#수정/삭제

요구사항

1. 수정은 POST 방식으로 처리 후 수정 결과를 확인할 수 있는 조회 화면으로 이동

2. 삭제는 POST 방식으로 처리 후 목록 화면으로 이동

3. 목록을 이동하는 작업은 GET 방식으로 처리 -> 기존에 사용하던 페이지 번호를 유지해서 이동

-> 수정과 삭제 모두 가능한 페이지에서 선택을 통해 이루어진다.

 

 

1.  컨트롤러에서 GET방식의 read()메서드에 어노테이션 값을 변경해 수정 URL 중 GET방식 처리

@Controller
@RequestMapping("/guestbook")
@Log4j2
@RequiredArgsConstructor
public class GuestbookController {

    @Autowired
    private final GuestbookService service;

	//조회, 수정
    @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);

    }
    // 1. get방식으로 값을 받아 Model에 DTO에 담아서 전달
    // 2. 나중에 목록 페이지에 돌아갈 데이터를 같이 저장하기 위해 PageRequestDTO를 파라미터로 사용
    // 3. @ModelAttribute는 없어도 되지만 requestDTO 이름으로 명시적으로 처리해 자동으로 requestDTO 값을 저장하도록 설정


}

 

2. modify.html은 read.html과 페이지가 유사하므로 복사해와서 변경작업을 거친다.

: 수정, 목록, 삭제 링크 추가

 

-> POST 방식을 사용하기 위해 form 태그로 수정하는 내용을 감싼다.

: <form action="~" method="post"> <class="form control"> <class="form-group"> </form>

-> 날짜는 JPA에서 자동처리되므로, name 속성이 존재하지 않는다.

 

modify.html

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">

<th:block th:replace="~{/layout/basic :: setContent(~{this::content} )}">

    <th:block th:fragment="content">

        <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}" >-->


            <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, '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>



    </th:block>

</th:block>

 

-> 해당하는 이벤트 처리를 위해 자바스크립트를 이용한다.

 

3. 서비스 계층에서 수정/삭제 메서드 구현

: 인터페이스에서 추상메서드 선언 후, 구현 클래스에서 해당 추상메서드 구현

public interface GuestbookService {

    void remove(Long gno);
    void modify(GuestbookDTO dto);
    
}
@Service
@Log4j2
//이 어노테이션은 초기화 되지않은 final 필드나, @NonNull 이 붙은 필드에 대해 생성자를 생성
@RequiredArgsConstructor
public class GuestbookServiceImpl implements GuestbookService{

    @Autowired
    private final GuestbookRepository repository;
    //리포지토리는 반드시 private final로 선언한다.
    
	@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);
        }
    }

 

4. 컨트롤러에서 삭제 URL 처리

: 삭제한 url정보를 가지고, 목록페이지로 리다이렉트

@Controller
@RequestMapping("/guestbook")
@Log4j2
@RequiredArgsConstructor
public class GuestbookController {

    @Autowired
    private final GuestbookService service;

//삭제
@PostMapping("/remove")
    public String remove(long gno, RedirectAttributes redirectAttributes){


        log.info("gno: " + gno);

        service.remove(gno);

        redirectAttributes.addFlashAttribute("msg", gno);

        return "redirect:/guestbook/list";

    }

 

삭제 이벤트 처리 [자바스크립트]

<script th:inline="javascript">
	var actionForm = $("form"); //form 태그 객체

            $(".removeBtn").click(function(){

                actionForm
                    .attr("action", "/guestbook/remove")
                    .attr("method","post");

                actionForm.submit();

            });
</script>

(1) remove 클릭시 : <form> 태그의 action, method 속성을 조정

(2) <input> 태그로 gno가 존재하므로, 해당 변수를 추출해 삭제시 이용

(3) 이후, 목록 1페이지로 이동

 

5. 컨트롤러에서 POST방식의 수정처리

: GET방식은 단순히 수정요청을 받았다면, POST 방식을 이용해 전달받은 값으로 변경 처리

@Controller
@RequestMapping("/guestbook")
@Log4j2
@RequiredArgsConstructor
public class GuestbookController {

    @Autowired
    private final GuestbookService service;

	//수정
    @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";

    }

 

POST 방식의 수정 메서드 파라미터 정보

(1) GuestbookDTO : 수정해야할 글의 정보

(2) PageRequestDTO : 기존 페이지 정보를 유지하는 PageRequestDTO

(3) RedirectAttributes : 조회 페이지로 다시 리다이렉트로 이동하기 위한 변수로, 페이지정보와 글번호 정보를 가지고 있다

 

-> 수정 작업이 진행 된 후에는 조회 페이지로 이동하는데,

RedirectAttributes를 이용. 기존 페이지 정보를 유지해 조회 페이지에서 다시 목록 페이지로 이동하는데 오류가 없도록 한다.

 

수정 이벤트 처리 [자바스크립트]

<script th:inline="javascript">

	$(".modifyBtn").click(function() {

                if(!confirm("수정하시겠습니까?")){
                    return ;
                }

                actionForm
                    .attr("action", "/guestbook/modify")
                    .attr("method","post")
                    .submit();
            });
</script>

 

6. 수정화면에서 다시 목록 페이지로 이동

페이지에서 이루어지는 작업은 모두 form태그를 이용하는데, 목록 페이지 이동시에는 page 파라미터만 필요하다.

-> 나머지 파라미터 제거

$(".listBtn").click(function() {

                var page = $("input[name='page']");
                // var type = $("input[name='type']");
                // var keyword = $("input[name='keyword']");

                actionForm.empty(); //form 태그의 모든 내용을 지우고

                actionForm.append(page); //목록 페이지 이동에 필요한 내용을 다시 추가
                // actionForm.append(type);
                // actionForm.append(keyword);


                actionForm
                    .attr("action", "/guestbook/list")
                    .attr("method","get");

			   console.log(actionForm.html()); //확인 후 주석처리
               actionForm.submit(); //확인후 주석 해제

            });

-> List 버튼 클릭시 원래 페이지로 이동 처리

더보기

Controllder, Service 

package com.boot2.guestbook.controller;

import com.boot2.guestbook.dto.GuestbookDTO;
import com.boot2.guestbook.dto.PageRequestDTO;
import com.boot2.guestbook.service.GuestbookService;
import lombok.RequiredArgsConstructor;
import lombok.extern.log4j.Log4j2;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;

@Controller
@RequestMapping("/guestbook")
@Log4j2
@RequiredArgsConstructor
public class GuestbookController {

    @Autowired
    private final GuestbookService service;

    //목록

    @GetMapping({"/"})
    public String list(){

        log.info("list..........");

        return "/guestbook/list";
    }
    @GetMapping("/list")
    public void list(PageRequestDTO pageRequestDTO, Model model){
        log.info("list.........."+pageRequestDTO);

        //model을 이용해 결과 데이터를 화면에 전달
        model.addAttribute("result", service.getList(pageRequestDTO));
    } //파라미터를 자동으로 수집하는 기능을 이용해 page와 size 파라미터를 전달하면 PageRequestDTO가 자동으로 수집된다.

    //등록
    @GetMapping("/register")
    public void register(){
        log.info("register get....");
    }

    @PostMapping("/register")
    public String registerPost(GuestbookDTO dto, RedirectAttributes redirectAttributes){
        log.info("dto..."+dto);

        //새로 추가된 엔티티 번호
        Long gno = service.register(dto);

        //리다이렉트 시에만 데이터 전달
        //addAttribute는 GET 방식이며 페이지를 새로고침 한다 해도 값이 유지된다.
        //addFlashAttribute는 POST 방식이며 이름처럼 일회성 데이터라 새로고침 하면 값이 사라진다.

        redirectAttributes.addFlashAttribute("msg", gno);

        return "redirect:/guestbook/list";
    }

    //조회, 수정
    @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);

    }
    // 1. get방식으로 값을 받아 Model에 DTO에 담아서 전달
    // 2. 나중에 목록 페이지에 돌아갈 데이터를 같이 저장하기 위해 PageRequestDTO를 파라미터로 사용
    // 3. @ModelAttribute는 없어도 되지만 requestDTO 이름으로 명시적으로 처리해 자동으로 requestDTO 값을 저장하도록 설정


    //삭제
    @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("type",requestDTO.getType());
//        redirectAttributes.addAttribute("keyword",requestDTO.getKeyword());

        redirectAttributes.addAttribute("gno",dto.getGno());


        return "redirect:/guestbook/read";

    }
}

 

package com.boot2.guestbook.service;

import com.boot2.guestbook.dto.GuestbookDTO;
import com.boot2.guestbook.dto.PageRequestDTO;
import com.boot2.guestbook.dto.PageResultDTO;
import com.boot2.guestbook.entity.Guestbook;
import org.springframework.stereotype.Service;

public interface GuestbookService {
    //GuestbookDTO 클래스의 인스턴스를 dto라고 이전에 선언했기 때문에
    //해당 클래스의 인스턴스명을 통일시키는 것이 가독성을 높이고 논리적 구조를 완성도를 높이는데 좋다.

    //인터페이스의 멤버 변수 public static final
    Long register(GuestbookDTO dto);

    //인터페이스의 추상 메서드 public abstract
    PageResultDTO<GuestbookDTO, Guestbook> getList(PageRequestDTO requestDTO);
    GuestbookDTO read(Long gno);
    void remove(Long gno);
    void modify(GuestbookDTO dto);

    //DTO -> Entity 변환 메서드 직접 작성
    default Guestbook dtoToEntity(GuestbookDTO dto) {
        Guestbook entity = Guestbook.builder()
                .gno(dto.getGno())
                .title(dto.getTitle())
                .content(dto.getContent())
                .writer(dto.getWriter())
                .build();
        return entity;
    }

    //Entity -> DTO 변환 메서드 직접 작성

    default GuestbookDTO entityToDto(Guestbook entity){

        GuestbookDTO dto  = GuestbookDTO.builder()
                .gno(entity.getGno())
                .title(entity.getTitle())
                .content(entity.getContent())
                .writer(entity.getWriter())
                .regDate(entity.getRegDate())
                .modDate(entity.getModDate())
                .build();

        return dto;
    }
}
package com.boot2.guestbook.service;

import com.boot2.guestbook.dto.GuestbookDTO;
import com.boot2.guestbook.dto.PageRequestDTO;
import com.boot2.guestbook.dto.PageResultDTO;
import com.boot2.guestbook.entity.Guestbook;
import com.boot2.guestbook.repository.GuestbookRepository;
import com.querydsl.core.BooleanBuilder;
import lombok.RequiredArgsConstructor;
import lombok.extern.log4j.Log4j2;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.stereotype.Service;

import javax.swing.text.html.Option;
import java.util.Optional;
import java.util.function.Function;

@Service
@Log4j2
//이 어노테이션은 초기화 되지않은 final 필드나, @NonNull 이 붙은 필드에 대해 생성자를 생성
@RequiredArgsConstructor
public class GuestbookServiceImpl implements GuestbookService{

    @Autowired
    private final GuestbookRepository repository;
    //리포지토리는 반드시 private final로 선언한다.

    //인터페이스에서 작성한 디폴트 메서드는 구현할 필요 없이 호출 가능하다.
    @Override
    public Long register(GuestbookDTO dto){
        log.info("DTO-----------------");
        log.info(dto);
        Guestbook entity=dtoToEntity(dto);

        log.info("DTOtoEntity-----------------");
        log.info(entity);

        //리포지토리에서 해당 엔티티를 저장하고, 글번호를 반환한다.
        repository.save(entity);
        return entity.getGno();
    }

    //PageRequestDTO에서 받은 DTO를 PageResultDTO로 전달하기 위해
    //PageResultDTO<> 인스턴스를 반환하는 getList() 구현
    @Override
    public PageResultDTO<GuestbookDTO, Guestbook> getList(PageRequestDTO requestDTO) {

        //검색조건 추가 전
        Pageable pageable = requestDTO.getPageable(Sort.by("gno").descending());

        Page<Guestbook> result = repository.findAll(pageable);

        //검색조건 추가 후
//        BooleanBuilder booleanBuilder = getSearch(requestDTO); //검색 조건 처리
//
//        Page<Guestbook> result = repository.findAll(booleanBuilder, pageable); //Querydsl 사용

        //일반적인 함수형태임으로 자주쓰이는 함수형 인터페이스: 하나의 매개변수를 받아 결과를 반환
        //Guestbook을 받아서 apply()메서드에 GuestbookDTO를 파라미터로 전달해 결과를 반환하는 형태
        //메서드가 하나뿐인 인터페이스이므로 메서드명을 생략해서 사용


        Function<Guestbook, GuestbookDTO> fn = (entity -> entityToDto(entity));

        return new PageResultDTO<>(result, fn );
    }
    @Override
    public GuestbookDTO read(Long gno){

        //Null 값과 널이 아닌 값 모두 담을 수 있기 때문에 사용
        Optional<Guestbook> result = repository.findById(gno);

        //널 체크 후 널 아니면 DTO로 변환해서 전달, null이면 null을 전달
        return result.isPresent()? entityToDto(result.get()):null;
    }

    @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);
        }
    }

}

  

 

더보기

Entity, DTO

package com.boot2.guestbook.entity;

import lombok.Getter;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;

import javax.persistence.Column;
import javax.persistence.EntityListeners;
import javax.persistence.MappedSuperclass;
import java.time.LocalDateTime;

//공통부분을 정의한 클래스 -> 객체로만 존재하는 가상 테이블
@MappedSuperclass

//리스너 : 언제 어떤 사용자가 삭제 요청했는지 로그로 남기는 이벤트 처리
//-> 엔티티 생명주기에 따른 이벤트 처리 가능하므로, 모든 기록을 리스너 하나로 처리 가능
//PostLoad : 엔티티가 영속성 컨텍스트에 조회된 직후, 또는 refresh를 호출한 후(2차 캐시에 저장되어 있어도 호출된다.)
//PrePersist : persist() 메소드를 호출해서 엔티티를 영속성 컨텍스트에 관리하기 직전에 호출된다. 식별자 생성 전략을 사용한 경우에는 엔티티의 식별자는 존재하지 않는 상태이다. 새로운 인스턴스를 merge 할 때도 수행된다.
//PreUpdate : flush나 commit을 호출해서 엔티티를 데이터베이스에 수정하기 직전에 호출된다.
//PreRemove : remove 메소드를 호출해서 엔티티를 영속성 컨텍스트에서 삭제하기 직전에 호출된다. 또한 삭제 명령어로 영속성 전이가 일어날 때도 호출된다. orphanRemoval 에 대해서는 flush나 commit 시에 호출된다.
//PostPrsist : flush나 commit을 호출해서 엔티티를 데이터베이스에 저장한 직후에 호출된다. 식별자가 항상 존재한다. 참고로 식별자 생성 전략이 IDENTITY인 경우 식별자를 생성하기 위해 persist()를 호출하면서 데이터베이스에 해당 엔티티를 저장하므로, 이때는 persist()를 호출한 직후에 바로 PostPersist가 호출된다.
//PostUpdate : flush나 commit을 호출해서 엔티티를 데이터베이스에 수정한 직후에 호출된다.(persist 시에는 호출되지 않는다)
//PostRemove : flush나 commit을 호출해서 엔티티를 데이터베이스에 삭제한 직후에 호출된다.

//적용 가능 위치
//1. 엔티티에 직접 적용
//2. 별도의 리스너 등록
//3. 기본 리스너 사용

//-> 댓글 작성 시간, 수정 시간 값 대입을 위해 리스너 사용
@EntityListeners(value = {AuditingEntityListener.class})
@Getter
public class BaseEntity {


    //엔티티 생성시간 : @CreatedDate
    //-> 디비 반영할 때 값 변경을 막기위해 updatable=false로 지정
    @CreatedDate
    @Column(name="regdate", updatable = false)
    private LocalDateTime regDate;

    //엔티티 최종 수정 시간 : @LastModifiedDate
    @LastModifiedDate
    @Column(name="moddate")
    private LocalDateTime modDate;
}

 

package com.boot2.guestbook.entity;

import lombok.*;

import javax.persistence.*;

//인자가 모두 있을 때의 생성자와, 기본 생성자 자동 생성
//toString() 메서드 자동생성
@Entity
@Getter
@Builder
@AllArgsConstructor @NoArgsConstructor
@ToString
public class Guestbook extends BaseEntity{

    //기본키
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long gno;

    @Column(length = 100, nullable = false)
    private String title;

    @Column(length = 1500, nullable = false)
    private String content;

    @Column(length = 50, nullable = false)
    private String writer;

    public void changeTitle(String title){
        this.title = title;
    }

    public void changeContent(String content){
        this.content = content;
    }

}

 

package com.boot2.guestbook.dto;

import lombok.*;

import javax.persistence.Column;
import java.time.LocalDateTime;

//DTO
//단점 : 엔티티와 중복 코드 작성
//장점 : 엔티티 객체 범위 제한, 화면과 데이터 분리에 최적화
// -> 뷰에서 접근과 변경이 가능하도록 해야하므로 엔티티와 다르게 Getter와 Setter 모두 생성
@Builder
//기본생성자와 모든 인자가 존재하는 생성자 자동 생성
@NoArgsConstructor
@AllArgsConstructor
@Data
public class GuestbookDTO {
    //엔티티와 똑같은 인스턴스 변수
    private Long gno;
    private String title;
    private String content;
    private String writer;
    private LocalDateTime regDate, modDate;

    //하지만, 엔티티와 다르게 DB에 접근하지 않고 화면 처리만을 위한 객체
    //-> 따라서 서비스 계층과 소통하면서 뷰를 처리하는데 최적화한다.

}

 

package com.boot2.guestbook.dto;

import lombok.*;

import javax.persistence.Column;
import java.time.LocalDateTime;

//DTO
//단점 : 엔티티와 중복 코드 작성
//장점 : 엔티티 객체 범위 제한, 화면과 데이터 분리에 최적화
// -> 뷰에서 접근과 변경이 가능하도록 해야하므로 엔티티와 다르게 Getter와 Setter 모두 생성
@Builder
//기본생성자와 모든 인자가 존재하는 생성자 자동 생성
@NoArgsConstructor
@AllArgsConstructor
@Data
public class GuestbookDTO {
    //엔티티와 똑같은 인스턴스 변수
    private Long gno;
    private String title;
    private String content;
    private String writer;
    private LocalDateTime regDate, modDate;

    //하지만, 엔티티와 다르게 DB에 접근하지 않고 화면 처리만을 위한 객체
    //-> 따라서 서비스 계층과 소통하면서 뷰를 처리하는데 최적화한다.

}
package com.boot2.guestbook.dto;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;


//화면에 전달되는 목록
@Builder
@AllArgsConstructor
@Data
public class PageRequestDTO {
    private int page;
    private int size;

    //화면에 전달되는 목록을 위한 생성자 : 1페이지부터 시작하고, 10페이지 단위로 목록 처리를 의미
    public PageRequestDTO(){
        this.page=1;
        this.size=10;
    }
    public Pageable getPageable(Sort sort){
        return PageRequest.of(page -1, size, sort);
        //정적으로 생성하기 위해 of를 사용하고, JPA에서 페이지는 0부터 시작하므로 page-1로 전달;
    }
}
package com.boot2.guestbook.dto;

import lombok.Data;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;

import java.util.List;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.IntStream;

//화면에 필요한 결과 목록
@Data

//DTO -> Entity로 변환하기 때문에
public class PageResultDTO<DTO, EN> {

    //DTO리스트
    private List<DTO> dtoList;

    //총 페이지 번호
    private int totalPage;

    //현재 페이지 번호
    private int page;
    //목록 사이즈
    private int size;

    //시작 페이지 번호, 끝 페이지 번호
    private int start, end;

    //이전, 다음
    private boolean prev, next;

    //페이지 번호  목록
    private List<Integer> pageList;


    //function 패키지에 있는 람다식으로 사용할 수 있는 편리한 함수형 메서드 :
    //매개변수와 반환값 존재 여부에 따라 구분 / 조건식 표현해 참 거짓 반환
    // Runnable -> run(), Supplier -> get(), Consumer -> accept(), Function -> apply() , Predicate -> test()

    //Page<Entity>의 객체들을 DTO객체로 변환해서 담는 메서드
    public PageResultDTO(Page<EN> result, Function<EN, DTO> fn){
        //페이징 결과를 스트림에 담아서, fn을 이용해 람다식을 통해 변환하고, 최종연산으로 리스트로 반환
        dtoList = result.stream().map(fn).collect(Collectors.toList());

        totalPage = result.getTotalPages();

        makePageList(result.getPageable());
    }//Function 함수형 인터페이스를 사용하면, 제네릭으로 정의되어 있기 때문에 어떤 엔티티를 전달해도 재사용 가능


    private void makePageList(Pageable pageable){

        this.page = pageable.getPageNumber() + 1; // JPA가 전달한 페이지는 0부터 시작하므로 1을 추가
        this.size = pageable.getPageSize();

        //임시 끝 번호
        int tempEnd = (int)(Math.ceil(page/10.0)) * 10;

        start = tempEnd - 9;

        prev = start > 1;

        //끝 번호 처리 : 임시 끝 번호와 크기 비교해 실제 마지막 번호와 임시 끝번호 중 선택
        end = totalPage > tempEnd ? tempEnd: totalPage;

        next = totalPage > tempEnd;


        //페이지 리스트
        //기본형 int스트림을 스트림으로 변환하는 메서드 : mapToObj()와 boxed()
        //int스트림을 이용해 범위 제한해 데이터를 추출하고, 다시 Stream<Integer>로 변환 후, 최종연산으로 담아서 리스트로 변환
        pageList = IntStream.rangeClosed(start, end).boxed().collect(Collectors.toList());
        //pageList는 List<Integer>
    }

}

 

더보기
package com.boot2.guestbook.repository;

public interface CustomRepository {
}
package com.boot2.guestbook.repository;

import com.querydsl.jpa.impl.JPAQueryFactory;
import lombok.RequiredArgsConstructor;

@RequiredArgsConstructor
public class CustomRepositoryImplRepository implements CustomRepository {
    private final JPAQueryFactory jpaQueryFactory;

}
package com.boot2.guestbook.repository;

import com.boot2.guestbook.entity.Guestbook;
import com.querydsl.core.BooleanBuilder;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.querydsl.QuerydslPredicateExecutor;

//Querydsl를 이용한 사용자 리포티조티에는 QuerydslPredicateExecutor<Guestbook> 인터페이스도 함께 상속한다.
public interface GuestbookRepository extends JpaRepository<Guestbook, Long>, CustomRepository , QuerydslPredicateExecutor<Guestbook> {

}

 

더보기

Views

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">

<th:block th:replace="~{/layout/basic :: setContent(~{this::content} )}">

    <th:block th:fragment="content">

        <h1 class="mt-4">GuestBook List Page
            <span>
                <a th:href="@{/guestbook/register}">
                    <button type="button" class="btn btn-outline-primary">REGISTER
                    </button>
                </a>
            </span>
        </h1>
<table class="table table-striped">
    <thead>
    <tr>
        <th scope="col">#</th>
        <th scope="col">Title</th>
        <th scope="col">Writer</th>
        <th scope="col">Regdate</th>
    </tr>
    </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>
        </th>
        <td>[[${dto.title}]]</td>
        <td>[[${dto.writer}]]</td>
        <td>[[${#temporals.format(dto.regDate, 'yyyy/MM/dd')}]]</td>
    </tr>



    </tbody>
</table>
        <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}) }" 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})}">
                    [[${page}]]
                </a>
            </li>

            <li class="page-item" th:if="${result.next}">
                <a class="page-link" th:href="@{/guestbook/list(page= ${result.end + 1} )}">Next</a>
            </li>

        </ul>

        <!-- 인라인 블록을 이용해 나타날 화면 -->
        <div class="modal" tabindex="-1" role="dialog">
            <div class="modal-dialog" role="document">
                <div class="modal-content">
                    <div class="modal-header">
                        <h5 class="modal-title">Modal title</h5>
                        <button type="button" class="close" data-dismiss="modal" aria-label="Close">
                            <span aria-hidden="true">&times;&times;</span>
                        </button>
                    </div>
                    <div class="modal-body">
                        <p>Modal body text goes here.</p>
                    </div>
                    <div class="modal-footer">
                        <button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
                        <button type="button" class="btn btn-primary">Save changes</button>
                    </div>
                </div>
            </div>
        </div>

        <!--부트스트랩 모달창을 이용해 등록 alert -->
        <!--th:inline을 이용해 타입 처리를 하지 않도록 설정 -->
        <!-- 단순 링크 이동의 경우 msg==null-->
        <script th:inline="javascript">

            var msg = [[${msg}]];

            console.log(msg);

            if(msg){
                $(".modal").modal();
            }
        </script>
</th:block>

</th:block>

 

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">

<th:block th:replace="~{/layout/basic :: setContent(~{this::content} )}">

    <th:block th:fragment="content">

        <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}" >-->


            <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, '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"); //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();
            });

            $(".listBtn").click(function() {

                //var pageInfo = $("input[name='page']");
                var page = $("input[name='page']");
                // var type = $("input[name='type']");
                // var keyword = $("input[name='keyword']");

                actionForm.empty(); //form 태그의 모든 내용을 지우고

                actionForm.append(page);
                // actionForm.append(type);
                // actionForm.append(keyword);


                actionForm
                    .attr("action", "/guestbook/list")
                    .attr("method","get");

               actionForm.submit();

            });

        </script>


    </th:block>

</th:block>

 

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">

<th:block th:replace="~{/layout/basic :: setContent(~{this::content} )}">

    <th:block th:fragment="content">

        <h1 class="mt-4">GuestBook Read Page</h1>

        <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}" readonly >
        </div>
        <div class="form-group">
            <label >Content</label>
            <textarea class="form-control" rows="5" name="content" readonly>[[${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" name="regDate" 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" name="modDate" th:value="${#temporals.format(dto.modDate, 'yyyy/MM/dd HH:mm:ss')}" readonly>
        </div>

<!--        <a th:href="@{/guestbook/modify(gno = ${dto.gno}, page=${requestDTO.page})}"><button type="button" class="btn btn-primary">Modify</button></a>-->

<!--        <a th:href="@{/guestbook/list(page=${requestDTO.page})}"><button type="button" class="btn btn-info">List</button></a>-->

        <!-- type과 keyword 처리-->
        <!-- , type=${requestDTO.type}, keyword =${requestDTO.keyword})-->

        <a th:href="@{/guestbook/modify(gno = ${dto.gno}, page=${requestDTO.page})}">
            <button type="button" class="btn btn-primary">Modify</button>
        </a>

        <a th:href="@{/guestbook/list(page=${requestDTO.page} )}">
            <button type="button" class="btn btn-info">List</button>
        </a>

    </th:block>

</th:block>

 

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">

<th:block th:replace="~{/layout/basic :: setContent(~{this::content} )}">

  <th:block th:fragment="content">

    <h1 class="mt-4">GuestBook Register Page</h1>

    <form th:action="@{/guestbook/register}" th:method="post">
      <div class="form-group">
        <label >Title</label>
        <input type="text" class="form-control" name="title" placeholder="Enter Title">
      </div>
      <div class="form-group">
        <label >Content</label>
        <textarea class="form-control" rows="5" name="content"></textarea>
      </div>
      <div class="form-group">
        <label >Writer</label>
        <input type="text" class="form-control" name="writer" placeholder="Enter Writer">
      </div>

      <button type="submit" class="btn btn-primary">Submit</button>
    </form>

  </th:block>

</th:block>

 

 

 

 

더보기

layout

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<!-- 부트스트랩 이용-->

<!-- 해당 파일에서 정의하는 컨텐츠 부분-->
<th:block th:fragment="setContent(content)">
    <head>

<head>

    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
    <meta name="description" content="">
    <meta name="author" content="">

    <title>Simple Sidebar - Start Bootstrap Template</title>

    <title>Simple Sidebar - Start Bootstrap Template</title>

    <!-- 타임리프를 이용한 링크 전달-->
    <!-- link th: href="@{static부터 시작되는 절대경로}" rel="사용할 이름"> -->

    <!-- Bootstrap core CSS -->
    <link th:href="@{/vendor/bootstrap/css/bootstrap.min.css}" rel="stylesheet">

    <!-- Custom styles for this template -->
    <link th:href="@{/css/simple-sidebar.css}" rel="stylesheet">

    <!-- Bootstrap core JavaScript -->
    <script th:src="@{/vendor/jquery/jquery.min.js}"></script>
    <script th:src="@{/vendor/bootstrap/js/bootstrap.bundle.min.js}"></script>

</head>

<body>

<div class="d-flex" id="wrapper">

    <!-- Sidebar -->
    <div class="bg-light border-right" id="sidebar-wrapper">
        <div class="sidebar-heading">Start Bootstrap </div>
        <div class="list-group list-group-flush">
            <a href="#" class="list-group-item list-group-item-action bg-light">Dashboard</a>
            <a href="#" class="list-group-item list-group-item-action bg-light">Shortcuts</a>
            <a href="#" class="list-group-item list-group-item-action bg-light">Overview</a>
            <a href="#" class="list-group-item list-group-item-action bg-light">Events</a>
            <a href="#" class="list-group-item list-group-item-action bg-light">Profile</a>
            <a href="#" class="list-group-item list-group-item-action bg-light">Status</a>
        </div>
    </div>
    <!-- /#sidebar-wrapper -->

    <!-- Page Content -->
    <div id="page-content-wrapper">

        <nav class="navbar navbar-expand-lg navbar-light bg-light border-bottom">
            <button class="btn btn-primary" id="menu-toggle">Toggle Menu</button>

            <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
                <span class="navbar-toggler-icon"></span>
            </button>

            <div class="collapse navbar-collapse" id="navbarSupportedContent">
                <ul class="navbar-nav ml-auto mt-2 mt-lg-0">
                    <li class="nav-item active">
                        <a class="nav-link" href="#">Home <span class="sr-only">(current)</span></a>
                    </li>
                    <li class="nav-item">
                        <a class="nav-link" href="#">Link</a>
                    </li>
                    <li class="nav-item dropdown">
                        <a class="nav-link dropdown-toggle" href="#" id="navbarDropdown" role="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
                            Dropdown
                        </a>
                        <div class="dropdown-menu dropdown-menu-right" aria-labelledby="navbarDropdown">
                            <a class="dropdown-item" href="#">Action</a>
                            <a class="dropdown-item" href="#">Another action</a>
                            <div class="dropdown-divider"></div>
                            <a class="dropdown-item" href="#">Something else here</a>
                        </div>
                    </li>
                </ul>
            </div>
        </nav>

        <div class="container-fluid">

            <th:block th:replace = "${content}"></th:block>

        </div>

    </div>
    <!-- /#page-content-wrapper -->

</div>
<!-- /#wrapper -->




<!-- Menu Toggle Script -->
<script>
    $("#menu-toggle").click(function(e) {
        e.preventDefault();
        $("#wrapper").toggleClass("toggled");
    });
</script>

</body>
</th:block>
</html>

 

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">

<!-- 부트스트랩에서 가져온 사이드바 이용-->
<!-- 해당 위치를 가리키는 ~{}-->
<!-- basic에 전달하는 파라미터로 content를 전송한다. 그 후 해당 basic으로 대체 즉, 여기 존재하는 content를 전달하고 basic에 넣어서 반환-->
<th:block th:replace="~{/layout/basic :: setContent(~{this::content} )}">


    <th:block th:fragment="content">

        <h1>exSidebar Page</h1>

    </th:block>

</th:block>

 

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">


<th:block th:fragment="setContent(content)">

    <head>
        <meta charset="UTF-8">
        <title>Title</title>
    </head>

<body>

<style>
    * {
        margin: 0;
        padding: 0;
    }
    .header {
        width:100vw;
        height: 20vh;
        background-color: aqua;
    }
    .content {
        width: 100vw;
        height: 70vh;
        background-color: lightgray;
    }
    .footer {
        width: 100vw;
        height: 10vh;
        background-color: green;
    }
</style>


<div class="header">
    <h1>HEADER</h1>
</div>
<div class="content" >

    <th:block th:replace = "${content}">
    </th:block>

</div>

<div class="footer">
    <h1>FOOTER</h1>
</div>

</body>
</th:block>
</html>
반응형