본문 바로가기

Server Programming/Spring Boot Backend Programming

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

반응형

 

요구사항

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

#목록 

 

1. DTO 구현 : 엔티티와 다르게 읽기/쓰기가 모두 가능하며 일회성 객체

 

엔티티는

JPA에서만 사용하는단순한 데이터를 담는 객체가 아니라 DB와 소통하는 수단이며, 엔티티 매니저가 관리하는 객체 :트랜잭션 범위에 해당

-> 영속성 컨텍스트안에서 영속 상태 여부 파악

-> 생명주기가 다른 DTO와 분리해서 처리해야한다.

 

package com.boot2.guestbook.dto;

import lombok.*;

import javax.persistence.Column;

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

}
더보기

2. 서비스 구현 : DTO를 전달받아 엔티티로 반환하고, 엔티티를 전달받아 DTO로 반환

-> 인터페이스를 이용해 서비스를 구현하기 때문에 GuestbookService와 GuestBookServiceImpl 작성

package com.boot2.service;

import com.boot2.guestbook.dto.GuestbookDTO;
import lombok.extern.log4j.Log4j2;
import org.springframework.stereotype.Service;

@Service
@Log4j2
public class GuestbookServiceImpl implements GuestbookService{
    @Override
    public Long register(GuestbookDTO dto){
        return null;
    }
}

 

 

3. 서비스에서 엔티티와 DTO의 변환 처리

방법:

1. 직접 처리

2. ModelMapper 라이브러리 이용

3. MapStruct 라이브러리 이용

 

-> 인터페이스에서 필요한 경우에만 가져다 쓸 수 있는 default 메서드 작성

: default 메서드를 이용하면 불필요한 추상클래스 작성을 줄일 수 있다.

 

package com.boot2.guestbook.service;

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

public interface GuestbookService {
    //GuestbookDTO 클래스의 인스턴스를 dto라고 이전에 선언했기 때문에
    //해당 클래스의 인스턴스명을 통일시키는 것이 가독성을 높이고 논리적 구조를 완성도를 높이는데 좋다.
    Long register(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;
    }
}

 

package com.boot2.guestbook.service;

import com.boot2.guestbook.dto.GuestbookDTO;
import com.boot2.guestbook.entity.Guestbook;
import lombok.extern.log4j.Log4j2;
import org.springframework.stereotype.Service;

@Service
@Log4j2
public class GuestbookServiceImpl implements GuestbookService{

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

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

        return null;
    }

}

-> 서비스 패키지위치 오류로 인한 버그 해결

 

 

4. Service에 만든 DTO->Entity메서드를 호출하는 ServiceImpl에서 JPA 처리를 위한 리포지토리를 주입한다.

: 리포지토리는 반드시 final로 주입하는데, final만 의존성 주입을 하는 어노테이션 @RequiredArgsConstructor를 이용한다.

-> 따라서 실제 서비스는 Impl이다. [@Service]

 

서비스 -> 리포지토리 : 엔티티 전달

리포지토리 -> 서비스 : 메서드에 해당하는 DTO 전달

package com.boot2.guestbook.service;

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

public interface GuestbookService {
    //GuestbookDTO 클래스의 인스턴스를 dto라고 이전에 선언했기 때문에
    //해당 클래스의 인스턴스명을 통일시키는 것이 가독성을 높이고 논리적 구조를 완성도를 높이는데 좋다.
    Long register(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.entity.Guestbook;
import com.boot2.guestbook.repository.GuestbookRepository;
import lombok.RequiredArgsConstructor;
import lombok.extern.log4j.Log4j2;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

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

}

 

 

5. GuestbookServiceTests에서 등록 테스트 수행

: 서비스 계층에서 DTO를 엔티티로 변환해 리포지토리로 전달하기위한 테스트

package com.boot2.guestbook.service;

import com.boot2.guestbook.dto.GuestbookDTO;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

@SpringBootTest
public class GuestbookServiceTests {

    @Autowired
    private GuestbookService service;

    @Test
    public void testRegister() {

        GuestbookDTO guestbookDTO = GuestbookDTO.builder()
                .title("Sample Title...")
                .content("Sample Content...")
                .writer("user0")
                .build();

        System.out.println(service.register(guestbookDTO));

    }
}

 

6. 목록 처리

: 페이징을 이용하는 목록을 작성하는데 재사용이 가능한 구조로 생성

 

(1) 페이지 요청 처리 DTO

-> 파라미터 : 페이지 번호, 페이지 내 목록 개수, 검색 조건

: 해당 파라미터를 가지는 DTO를 만들어 필요할 때 재사용하는 용도로 사용한다.

 

화면에 전달 되는 목록 DTO : PageRequestDTO
화면에 필요한 결과 목록 DTO : PageResultDTO

Page<Entity> -> DTO : PageReusltDTO

DTO -> Page<Entity> : GuestbookServiceImpl
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);

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

}

 

 

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 java.util.List;
import java.util.function.Function;
import java.util.stream.Collectors;

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

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


    private List<DTO> dtoList;

    //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());

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

 

(1) Service : 인터페이스에 PageResultDTO를 반환하는 getList() 추상메서드 작성

(2) ServiceImpl: 인터페이스의 getList() 메서드 구현

 

Function 함수형 인터페이스를 이용해 <Guestbook, GuestbookDTO>

fn : GuestbookDTO entitytoDto(Guestbook)

 

requestDTO를 페이징과 정렬을 수행해서,

-> gno를 내림차순으로 정렬하고, 해당 1부터 10까지 페이징을 이용해 findAll()로 찾아서 Page<Guestbook>에 담는다.

 

PageResultDTO<>로 전달

기본형 : public class PageResultDTO<DTO, EN>

반환형 : return new PageResultDTO<>(result, fn)

 

7. 목록 처리 테스트

: 요청 받은 목록인 PageRequestDTO를 getList를 통해 페이징, 정렬, entityToDto 수행한 목록 처리 테스트

 

@SpringBootTest
public class GuestbookServiceTests {

    @Autowired
    private GuestbookService service;

    @Test
    public void testList() {

    PageRequestDTO pageRequestDTO = PageRequestDTO.builder().page(1).size(10).build();

    PageResultDTO<GuestbookDTO, Guestbook> resultDTO = service.getList(pageRequestDTO);

    for (GuestbookDTO guestbookDTO : resultDTO.getDtoList()) {
        System.out.println(guestbookDTO);
    }
}

 

 

8. 목록 데이터 페이지 처리

: 화면까지 전달되는 데이터인 PageResultDTO를 이용해 화면에서 페이지 처리

: 시작 페이지, 끝 페이지 등 필요한 정보를 담는다.

화면에서 시작 페이지 번호(start)
화면에서 끝 페이지 번호 (end)
이전/다음 이동 링크 여부 (prev, next)
현재 페이지 번호 (page)
요구사항
1. 10개씩 페이지 번호를 출력
2. 1부터 10까지는 이전으로 가는 링크가 안나오도록
3. 10페이지 이후에만 이전으로 가는 링크 생성
4. 마지막 페이지의 링크 계산

 

구현 순서
1. 임시 끝 번호를 먼저 계산
tempEnd = (int) (Math.ceil(페이지 번호 / 10.0)) * 10;
-> Math함수는 반환형이 Double이므로 int로 명시적 형변환 해주며,
페이지번호를 10으로 나누고 소수점으로 올림 후, 10을 곱하면 임시 끝번호

2. 시작 번호는 끝번호 -9
start = tempEnd - 9;

3. 끝 번호는 올림한 끝 번호이기 때문에, 실제 마지막 페이지는 따로 구한다.
totalPage = result.getTotalPages();
end = totalPage > tempEnd ? tempEnd : totalPage;
-> 실제 마지막 번호가 임시 끝번호보다 클 경우 실제 마지막 번호를 끝 번호로 처리하고, 작을 경우 실제 마지막 번호를 끝으로 처리한다.

즉, 끝을 나타내는 번호인 3개의 변수
(1) tempEnd 임시 끝 번호         : 소수점 올림한 번호
(2) totalPage 실제 마지막 번호 : 임시 끝번호와 비교해 끝 번호 처리
(3) end 끝 번호

4. 이전과 다음 처리
prev = start > 1;
-> 현재 페이지가 2이상일 때 [JPA는 0부터 시작하지만, 이미 -1처리를 한 상태이므로]
next = totalPage > tempEnd;
-> 실제 마지막 페이지가 임시 끝번호 보다 클 때만

 

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

}

 

9. 페이징 처리한 실제 목록 출력하는 테스트

@SpringBootTest
public class GuestbookServiceTests {

    @Autowired
    private GuestbookService service;


//요청 받은 목록을 getList를 통해 페이징, 정렬, entityToDto 수행한 목록 처리 테스트
    @Test
    public void testList(){

        PageRequestDTO pageRequestDTO = PageRequestDTO.builder().page(1).size(10).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));
    }

 

10. 컨트롤러에서 화면의 목록 처리

 

(1) 리스트 출력

[[]] : 인라인 표현식으로 태그 없이 화면에 엔티티를 출력가능 [[${엔티티.변수}]]

더보기

th:scope는 해당 헤더 셀의 종류를 명시한다.

 

<th scope="col|row|colgroup|rowgroup">

속성값

속성값 설명
col   해당 셀이 열(column)을 위한 헤더 셀임을 명시함.
row   해당 셀이 행(row)을 위한 헤더 셀임을 명시함.
colgroup   해당 셀이 열의 그룹을 위한 헤더 셀임을 명시함.
rowgroup   해당 셀이 행의 그룹을 위한 헤더 셀임을 명시함.

: each을 이용해 출력하고, #temporals.format을 이용해 날짜 처리

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

 

11. 화면에서 목록 페이지 처리

/guestbook/list
/guestbook/list?page=1

: 1페이지 출력

/guestbook/list?page=2

:2페이지 출력

 

 <이전> 1 ~ 10 <다음>

(1) 이전과 다음은 타임리프의 if를 이용해 노출처리

(2) 현재페이지를 active로 처리해서 현재페이지 색깔 처리

(3) 페이지 번호 링크 처리는 타임리프의 href=@{...}이용

[이전의 경우에는 DTO의 start값보다 -1 / 다음의 경우에는 DTO의 end값보다 +1]

 

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

 

https://getbootstrap.kr/docs/5.0/components/pagination/

 

페이지네이션

여러 페이지에 일련의 관련 내용이 있음을 나타내는 페이지네이션을 사용한 문서와 예시입니다.

getbootstrap.kr

 

-> 부트스트랩의 페이지네이션을 사용하는 기법으로, class명으로 page-item active를 전달하면 현재 페이지를 나타낸다.

<li class="page-item active">
      <a class="page-link" href="#">2 <span class="sr-only">(current)</span></a>
    </li>

# 등록

1. 컨르롤러에서 URL 처리

: 리다이렉션시에만 데이터 전달

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

    @Autowired
    private final GuestbookService service;

	//등록
    @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";
    }
}

 

<!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>

-> 등록 후에는 다시 목록 페이지로 이동하는데, 작성한 글 번호를 전달한다.

-> model에 글번호가 담겨있는 상태

 

2. 등록 처리와 목록 페이지의 모달창

: 부트스트랩의 모달창을 이용해 처리 결과를 alert

https://getbootstrap.kr/docs/5.0/components/modal/

 

모달

Bootstrap JavaScript 모달 플러그인을 사용하여 라이트박스, 사용자 알림 또는 사용자 정의 콘텐츠를 만들 수 있습니다.

getbootstrap.kr

 

스크립트 부분

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

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

            console.log(msg);
            
            if(msg){
                $(".modal").modal();
            }
        </script>

 

인라인으로 만들었기 때문에 해당 페이지를 삽입

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

 

3. 등록 페이지 링크와 조회 페이지 링크 처리

 

등록 링크

<span>
    <a th:href="@{/guestbook/register}">
        <button type="button" class="btn btn-outline-primary">REGISTER
        </button>
    </a>
</span>

 

조회 링크

<tr th:each="dto : ${result.dtoList}" >
        <th scope="row">
            <a th:href="@{/guestbook/read(gno = ${dto.gno},
                    page= ${result.page})}">
                [[${dto.gno}]]
            </a>
        </th>
        <td>[[${dto.title}]]</td>
        <td>[[${dto.writer}]]</td>
        <td>[[${#temporals.format(dto.regDate, 'yyyy/MM/dd')}]]</td>
    </tr>

 

list.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 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>
반응형