공유메모장
[Spring Boot Project] 영화 목록 처리와 페이지 처리 본문
영화 목록 처리
테이블 구조
현재 데이터베이스에는 Movie 테이블에 데이터가 100개 + a , Movie_image 테이블에 데이터가 300 + b 개 있다. review 테이블에는 200 + c 개, m_member 테이블에 100개의 데이터가 있다.
목록 데이터는 영화 제목과 이미지, 리뷰 개수, 평균 평점으로 구성되며 이를 화면에 출력한다. 화면에 목록과 페이지 처리를 하기 위해서는 Page와 Pageable을 파라미터로 받고, 리턴하는 PageRequestDTO, PageResultDTO가 필요하다.
PageRequestDTO
package com.example.mreview.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
@Data
@AllArgsConstructor
public class PageRequestDTO<DTO, EN> {
private int page;
private int size;
private String type;
private String keyword;
public PageRequestDTO(){ //초기값
this.page = 1;
this.size = 10;
}
public Pageable getPageable(Sort sort){
//이 메소드가 pageRequestDTO의 진짜목적. JPA쪽에서 사용하는 Pageable 타입의 객체를 생성
//sort만 별도의 파라미터로 받는 이유: 나중에 다양한 상황에서 사용하기 위해
return PageRequest.of(page -1, size, sort);
}
}
PageRequestDTO는 Pageable 객체를 리턴한다는 것에 의미가 있다. PageRequest.of() 메소드를 통해 페이징 정보를 리턴한다. type과 keyword 필드는 검색을 위한 필드인데, 아직 사용되지 않으니 무시해도 좋다.
여러 곳에서도 유연하게 사용되기 위해서 제네릭타입을 선택했다.
PageResultDTO
package com.example.mreview.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
public class PageResultDTO<DTO, EN> { //EN == ENTITY
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; //페이지 번호 목록
public PageResultDTO(Page<EN> result, Function<EN,DTO> fn){
//Function 함수형인터페이스는 apply메서드를 의미하며,
//PageResultDTO가 사용될 때 람다를 이용하여 apply를 구현할 것이다.
dtoList = result.stream().map(fn).collect(Collectors.toList());
totalPage = result.getTotalPages();
makePageList(result.getPageable());
}
private void makePageList(Pageable pageable){
this.page= pageable.getPageNumber() +1;
this.size= pageable.getPageSize();
//temp end page
int tempEnd = (int)(Math.ceil(page/10.0)) *10; //1페이지 당 10개씩 보여준다는 가정 하에
start = tempEnd - 9;
prev = start > 1; //만약 시작페이지가 1보다 크면 prev를 보여준다.
end = totalPage > tempEnd ? tempEnd : totalPage; //만약 토탈페이지가 34이면, tempEnd는 40일테니까, tempEnd대신 토탈페이지 값을 end로 사용한다.
next = totalPage > tempEnd;
pageList= IntStream.rangeClosed(start,end).boxed().collect(Collectors.toList());
}
}
PageResultDTO는 많은 정보를 담아야 한다. PageResultDTO에 실려오는 정보로 페이징을 처리할 것이기 때문이다.
우선 생성자 부분부터 보도록 한다.
public PageResultDTO(Page<EN> result, Function<EN,DTO> fn){
//Function 함수형인터페이스는 apply메서드를 의미하며,
//PageResultDTO가 사용될 때 람다를 이용하여 apply를 구현할 것이다.
dtoList = result.stream().map(fn).collect(Collectors.toList()); //List<DTO>
totalPage = result.getTotalPages();
makePageList(result.getPageable());
}
PageResultDTO는 어떠한 쿼리의 결과로 만들어진 Page를 받는다. Page는 특정 Entity 타입일수도, Object 일수도, Object[] 일수도 있다. Function<EN,DTO> fn 은 Page 객체를 처리할 방법을 다른 곳에서 주입 받겠다는 뜻이다. Function을 사용함으로써 해당 메소드는 더 다양한 Entity와 DTO 쌍에 대해 처리할 수 있게 된다.
파라미터로 넘어온 fn에 정의된 로직에 따라 map()으로 데이터 변환 처리를 하고, Collectors.toList()로 이 각각의 변환된 데이터를 List<DTO>로 만든다.
또, result는 페이징 정보를 갖고 있어서 getTotalPages() 메소드로 페이지 개수를 받을 수 있다.
private void makePageList(Pageable pageable){
this.page= pageable.getPageNumber() +1;
this.size= pageable.getPageSize();
//temp end page
int tempEnd = (int)(Math.ceil(page/10.0)) *10; //1페이지 당 10개씩 보여준다는 가정 하에
start = tempEnd - 9;
prev = start > 1; //만약 시작페이지가 1보다 크면 prev를 보여준다.
end = totalPage > tempEnd ? tempEnd : totalPage; //만약 토탈페이지가 34이면, tempEnd는 40일테니까, tempEnd대신 토탈페이지 값을 end로 사용한다.
next = totalPage > tempEnd;
pageList= IntStream.rangeClosed(start,end).boxed().collect(Collectors.toList());
}
}
makePageList 메소드는 pageable 객체를 받아서, 실제로 화면에서 페이지 넘김에 대한 데이터 처리를 수행한다.
page와 size는 pageable 객체에 들어가 있기 때문에 꺼내온다. (pageable은 0부터 시작하기 때문에 1을 더해서 page가 1부터 시작되도록 한다.)
한 페이지당 목록바를 1부터 2, 3, 4 ....10 까지 보여준다는 가정 하에 tempEnd 는 Math.ceil(page/10.0)) * 10 이 된다.
이게 어떤 의미인가 하면, 우선 tempEnd는 아래의 그림에서 '10'을 의미한다. 즉, 해당 목록바의 끝을 의미하게 된다.
현재 데이터베이스에 Movie 데이터가 100 + a 개 만큼 있기 때문에, 10개의 데이터를 11페이지에 모두 표현할 수 있다.
page가 1이라는 가정하에 10.0을 나누면 0.1이 되고, 이에 Math.ceil()을 적용하면 1.0이 된다. 여기에 10을 곱하면 10.0, int로 형변환하면 10이 된다.
page가 2,3,4 ... 9, 10일 때 까지 10으로 나눈 값은 0.x이기 때문에 올림을 하고 10을 곱한 값이 무조건 10일 수 밖에 없다. 즉, tempEnd가 10이다.
그렇다면 page가 11이라면? 10으로 나눈 값이 1.1이 되고 올림한 값이 2.0이 되고 10을 곱한 값이 20이 된다.
즉, tempEnd가 20이 된다. 이런식으로 몇 페이지냐에 따라 목록바를 변화시킬 수 있도록 한다.
반대로 start는 그것에 9를 뺀 값인 1이 된다.
만약 목록바를 5개씩 본다고 지정한다면 목록바는 아래와 같이 구성된다.
tempEnd는 목록바의 끝, 그렇다면 start는 목록바의 처음이 된다.
위에서 왜 목록바의 끝을 tempEnd로 두었을까? 그것은 진짜 "끝"이 아니기 때문이다. 위에서 예시를 든 것처럼, 페이지가 11이라면 목록바의 끝은 20이 되어야한다. 하지만 데이터가 그만큼 존재하지 않기 때문에 12~20까지의 페이지는 공백페이지 또는 목록이 조회되지 않아 문제가 발생할 것이다. 그렇기 때문에 진짜 "끝"을 정의해야 한다.
위에서 pageable 객체로부터 totalPage를 받았다. 이를 이용해서 tempEnd가 totalPage보다 작으면 tempEnd보다는 보여줄 데이터가 더 많다는 뜻이고, totalPage보다 tempEnd가 더 커지면 데이터가 tempEnd만큼 없다는 뜻이 된다. 그렇기 때문에 end = totalPage > tempEnd ? tempEnd : totalPage; 처럼 삼항연산자를 이용해서 진짜 끝인 end를 정의한다.
prev는 목록바를 앞으로 돌리는 버튼인데, 페이지 상에서는 Previous라 표현하고, start가 1이 아니라면 보이도록 한다.
Next는 목록바를 뒤로 돌리는 버튼인데, 페이지 상에서는 Next라 표현한다. totalPage보다 tempEnd가 더 크다면 아직까지는 next 버튼이 존재할 수 있다.
pageList는 위의 목록바를 생성하는 코드이다.
MovieRepository
public interface MovieRepository extends JpaRepository<Movie, Long> {
//coalesce 는 null 값을 설정한 값으로 변환해주는 함수. 해당 예제에서는 r.grade가 null이면 0이라는 값으로 치환한다.
// @Query("select m, avg(coalesce(r.grade,0)), count(distinct r) from Movie m " +
// "left outer join Review r on r.movie = m group by m")
// Page<Object[]> getListPage(Pageable pageable);
// max(mi) 로 이미지 1개를 가져오면 N+1 문제가 발생한다. join 후에 또 다시 max(mi)를 찾기위해 쿼리가 n번 실행되기 때문이다.
// 추측: max(mi)를 찾는 query = select mi.(mi의 pk값) from mi orber by asc limit 1; //페이징이 10이면 10번 수행됨
// @Query("select m, max(mi), avg(coalesce(r.grade,0)), count(distinct r) from Movie m "+
// "left outer join MovieImage mi on mi.movie = m " +
// "left outer join Review r on r.movie = m group by m")
// Page<Object[]> getListPage(Pageable pageable);
@Query("select m, mi, avg(coalesce(r.grade,0)), count(distinct r) from Movie m "+
"left outer join MovieImage mi on mi.movie = m " +
"left outer join Review r on r.movie = m group by m")
Page<Object[]> getListPage(Pageable pageable);
}
주석으로 달아둔 부분은 추후 자세히 다룬 후에 포스팅 할 예정이다.
MovieImage과 Review를 left outer join한다. left outer join한다는 것은, from으로 지정된 Movie 클래스의 데이터와 MovieImage 클래스의 데이터를 결합하는데, 조건은 movie 테이블의 id와 movieImage의 id가 같은 경우에 결합한다는 것이다. review의 경우도 동일하다. left outer join이기 때문에 movieImage가 특정 movie에 대한 fk를 갖는 데이터가 하나도 없을 지라도 Movie Entity는 select된다. Review의 경우도 마찬가지다.
select 되는 것들은 Movie의 엔티티, MovieImage의 엔티티, Riview의 grade 필드 값 중, null값이라면 0으로 치환하여 평균을 구한 값, 리뷰의 개수 이다.
MovieService
MovieService의 인터페이스로, 필요한 메소드들이 선언되어있다. 예외적으로, Service -> Repository 로 들어갈 때 DTO -> Entity 변환, Repository->Service로 나갈 때 Entity -> DTO 변환 을 담당하는 메소드는 default 메소드로 선언하여 구체 메소드로 사용할 수 있다.
package com.example.mreview.service;
import com.example.mreview.dto.MovieDTO;
import com.example.mreview.dto.MovieImageDTO;
import com.example.mreview.dto.PageRequestDTO;
import com.example.mreview.dto.PageResultDTO;
import com.example.mreview.entity.Movie;
import com.example.mreview.entity.MovieImage;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
public interface MovieService {
Long register(MovieDTO movieDTO);
PageResultDTO<MovieDTO, Object[]> getList(PageRequestDTO pageRequestDTO);
default Map<String, Object> dtoToEntity(MovieDTO movieDTO){
//Map타입으로 반환
Map<String, Object> entityMap= new HashMap<>();
Movie movie = Movie.builder()
.mno(movieDTO.getMno())
.title(movieDTO.getTitle())
.build(); //dto에서 빼낼 수 있는 정보는 빼서
entityMap.put("movie",movie); //hashMap으로 만듦.
List<MovieImageDTO> imageDTOList = movieDTO.getImageDTOList(); //dto에서 imageList를 받음
//MovieImageDTO 처리
if(imageDTOList != null && imageDTOList.size() > 0){
List<MovieImage> movieImageList = imageDTOList.stream().map(movieImageDTO -> {
MovieImage movieImage = MovieImage.builder()
.path(movieImageDTO.getPath())
.imgName(movieImageDTO.getImgName())
.uuid(movieImageDTO.getUuid())
.movie(movie)
.build();
return movieImage;
}).collect(Collectors.toList());
entityMap.put("imgList", movieImageList); //한 번에 두가지 종류의 객체를 반환하기 위해 Map타입 사용
// ex) Movie register할 때 hashMap 내용물 : "movie" 이름의 movie 엔티티 객체, imgList 이름의 movieImage 엔티티 리스트
}
return entityMap;
}
default MovieDTO entitiesToDTO(Movie movie, List<MovieImage> movieImages, Double avg, Long reviewCnt){
MovieDTO movieDTO = MovieDTO.builder()
.mno(movie.getMno())
.title(movie.getTitle())
.regDate(movie.getRegDate())
.modDate(movie.getModDate())
.build();
List<MovieImageDTO> movieImageDTOList = movieImages.stream().map(movieImage ->
{
return MovieImageDTO.builder().imgName(movieImage.getImgName())
.path(movieImage.getPath())
.uuid(movieImage.getUuid())
.build();
}).collect(Collectors.toList());
movieDTO.setImageDTOList(movieImageDTOList);
movieDTO.setAvg(avg);
movieDTO.setReviewCnt(reviewCnt.intValue());
return movieDTO;
}
}
MovieDTO는 원래 avg, reviewCnt, regDate 라는 필드가 없었다. 이전포스트
하지만 필요한 데이터를 추출하다 보니 avg, reviewCnt, regDate라는 필드가 필요하게 되어 새롭게 추가했다.
package com.example.mreview.dto;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class MovieDTO {
private Long mno;
private String title;
@Builder.Default
private List<MovieImageDTO> imageDTOList = new ArrayList<>();
private double avg;
private int reviewCnt;
private LocalDateTime regDate;
private LocalDateTime modDate;
}
MovieServiceImpl
MovieService 인터페이스를 구현한 클래스이다.
package com.example.mreview.service;
import com.example.mreview.dto.MovieDTO;
import com.example.mreview.dto.PageRequestDTO;
import com.example.mreview.dto.PageResultDTO;
import com.example.mreview.entity.Movie;
import com.example.mreview.entity.MovieImage;
import com.example.mreview.repository.MovieImageRepository;
import com.example.mreview.repository.MovieRepository;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
@Service
@RequiredArgsConstructor
@Slf4j
public class MovieServiceImpl implements MovieService{
private final MovieRepository movieRepository;
private final MovieImageRepository movieImageRepository;
@Transactional
@Override
public Long register(MovieDTO movieDTO) {
Map<String,Object> entityMap = dtoToEntity(movieDTO);
Movie movie = (Movie) entityMap.get("movie");
List<MovieImage> movieImageList = (List<MovieImage>) entityMap.get("imgList");
movieRepository.save(movie);
movieImageList.forEach(movieImage -> {
movieImageRepository.save(movieImage);
});
return movie.getMno();
}
public PageResultDTO<MovieDTO, Object[]> getList(PageRequestDTO requestDTO){
Pageable pageable= requestDTO.getPageable(Sort.by("mno").descending());
Page<Object[]> result = movieRepository.getListPage(pageable);
Function<Object[],MovieDTO> fn = ( arr->entitiesToDTO(
(Movie) arr[0],
(List<MovieImage>)(Arrays.asList((MovieImage)arr[1])), //Arrays.asList는 일반 배열을 ArrayList로 바꿔준다
(Double) arr[2],
(Long)arr[3])
);
return new PageResultDTO<>(result,fn);
}
}
getList 메소드를 보면, return 타입은 PageResultDTO<MovieDTO, Obejct[]> 이며 PageRequestDTO를 파라미터로 받는다. 이 PageRequestDTO에는 page, size, 등의 정보가 들어있다. requestDTO에 getPageable은, sort 객체를 생성해서 주면 해당 정렬 기준을 적용한 pageable 객체를 반환해준다. 이 pageable 객체를 repository에 전달하여 jpql을 수행하고, 그 결는 Page<Object[]>의 형태로 반환된다.
Function을 정의하는데, Object[] 를 MovieDTO로 변환하는 fn을 정의한다. jqpl에 작성한 순서대로, object[0]은 Movie 엔티티, object[1]은 MovieImage 엔티티 리스트(복수 개) object[2]에는 리뷰 평균 평점, object[3]에는 리뷰 개수가 들어가 있을 것이다. 데이터와 fn을 PageResultDTO에 실어서 반환한다.
fn에 대한 동작을 서술해보자면, result를 arr라는 파라미터로 받아서 가공하여 entitiesToDTO 메소드의 파라미터로 사용한다. entitiesToDTO의 파라미터로 (Movie) arr[0] , (List<MovieIamge>)(Arrays.asList((MOvieImage)arr[1])), (Double) arr[2], (Long) arr[3] 이 들어간다.
PageResultDTO의 생성자에 아래와 같은 내용이 정의되어있기 때문에, 사용자가 하지 않아도 자동으로 Object[]에 fn을 적용시켜서 dtoList를 생성해낸다.
public PageResultDTO(Page<EN> result, Function<EN,DTO> fn){ //Function 함수형인터페이스는 apply메서드를 의미하며,
//PageResultDTO가 사용될 때 람다를 이용하여 apply를 구현할 것이다.
dtoList = result.stream().map(fn).collect(Collectors.toList());
totalPage = result.getTotalPages();
makePageList(result.getPageable());
}
MovieController
package com.example.mreview.controller;
import com.example.mreview.dto.MovieDTO;
import com.example.mreview.dto.PageRequestDTO;
import com.example.mreview.service.MovieService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.domain.Page;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;
@Controller
@RequestMapping("/movie")
@Slf4j
@RequiredArgsConstructor
public class MovieController {
private final MovieService movieService;
@GetMapping("/register")
public void register(){
}
@PostMapping("/register")
public String register(MovieDTO movieDTO, RedirectAttributes redirectAttributes){
log.info("movieDTO: "+movieDTO);
Long mno = movieService.register(movieDTO);
redirectAttributes.addFlashAttribute("msg",mno);
return "redirect:/movie/list";
}
@GetMapping("list")
public void list(PageRequestDTO pageRequestDTO, Model model){
log.info("pageRequestDTO: "+ pageRequestDTO);
model.addAttribute("result", movieService.getList(pageRequestDTO));
}
}
컨트롤러에서는 Service를 호출하여 위의 과정을 수행하도록 한다. 그리고, model에 result라는 이름의 PageResultDTO를 붙여서 list 페이지로 이동한다.
list.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">Movie List Page
<span>
<a th:href="@{/movie/register}">
<button type="button" class="btn btn-outline-primary">REGISTER
</button>
</a>
</span>
</h1>
<form action="/movie/list" method="get" id="searchForm">
<input type="hidden" name="page" value="1">
</form>
<table class="table table-striped">
<thead>
<tr>
<th scope="col">#</th>
<th scope="col">Picture</th>
<th scope="col">Review Count</th>
<th scope="col">AVG Rating</th>
<th scope="col">Regdate</th>
</tr>
</thead>
<tbody>
<tr th:each="dto : ${result.dtoList}" >
<th scope="row">
<a th:href="@{/movie/read(mno = ${dto.mno}, page= ${result.page})}">
[[${dto.mno}]]
</a>
</th>
<td><img th:if="${dto.imageDTOList.size() > 0 && dto.imageDTOList[0].path != null }"
th:src="|/display?fileName=${dto.imageDTOList[0].getThumbnailURL()}|" >[[${dto.title}]]</td>
<td><b>[[${dto.reviewCnt}]]</b></td>
<td><b>[[${dto.avg}]]</b></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="@{/movie/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="@{/movie/list(page = ${page})}">
[[${page}]]
</a>
</li>
<li class="page-item" th:if="${result.next}">
<a class="page-link" th:href="@{/movie/list(page= ${result.end + 1} )}">Next</a>
</li>
</ul>
<script th:inline="javascript">
</script>
</th:block>
</th:block>
데이터 목록 화면
<table class="table table-striped">
<thead>
<tr>
<th scope="col">#</th>
<th scope="col">Picture</th>
<th scope="col">Review Count</th>
<th scope="col">AVG Rating</th>
<th scope="col">Regdate</th>
</tr>
</thead>
<tbody>
<tr th:each="dto : ${result.dtoList}" >
<th scope="row">
<a th:href="@{/movie/read(mno = ${dto.mno}, page= ${result.page})}">
[[${dto.mno}]]
</a>
</th>
<td><img th:if="${dto.imageDTOList.size() > 0 && dto.imageDTOList[0].path != null }"
th:src="|/display?fileName=${dto.imageDTOList[0].getThumbnailURL()}|" >[[${dto.title}]]</td>
<td><b>[[${dto.reviewCnt}]]</b></td>
<td><b>[[${dto.avg}]]</b></td>
<td>[[${#temporals.format(dto.regDate, 'yyyy/MM/dd')}]]</td>
</tr>
</tbody>
</table>
th:each 구문을 이용하여 result에 담긴 dtoList를 하나씩 꺼내온다. 꺼낸 dtoList의 요소는 dto라는 이름을 가진다.
movie의 id, 즉 pk가 표현되는 공간에는 <a> 태그를 넣어서 클릭 시 상세조회가 가능하도록 한다. 상세 조회 페이지에서 다시 목록으로 돌아올 때 page 번호가 필요하기 때문에 page번호를 url에 유지시킨다.
<img> 태그는 만약 dto가 이미지를 포함하고 있지 않은 경우에는 img 태그를 포함하지 않는다. 이미지를 포함한 경우에만 display 컨트롤러로 넘어가서 파일에 대한 decoding 처리와 byte변환을 거쳐 사진으로 표현이 될 것이다.
사진 옆에 타이틀을 붙여준다.
그 아래의 코드로는 리뷰 개수 , 평균 평점, 등록날짜가 되겠다.
목록바 처리
<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="@{/movie/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="@{/movie/list(page = ${page})}">
[[${page}]]
</a>
</li>
<li class="page-item" th:if="${result.next}">
<a class="page-link" th:href="@{/movie/list(page= ${result.end + 1} )}">Next</a>
</li>
</ul>
th:if 태그를 이용하여 만약 result.prev 값이 true라면, page = start -1 로 이동하는 링크를 보여준다는 뜻이다.
밑의 li 태그에서는 지금 클릭된 페이지를 파란색 버튼으로 바꾸는 작업을 하고 있으며, th:each 를 이용해서 pageList를 하나씩 꺼내 목록바로 구성하고있다. 목록바의 page 하나하나에 링크가 걸려서, page의 값을 url로 전송하면 페이지에 따른 데이터 목록이 조회되도록 한다.
마지막 li 태그는 next가 true라면 Next 버튼이 보이도록 한다.
모든 목록바 버튼에는 링크가 달려있으며, 링크는 모두 page 번호를 url로 가지고 있다.
해당 포스트는 코드로 배우는 스프링 웹 프로젝트 개정판을 기반으로 작성하였습니다.
'프로젝트' 카테고리의 다른 글
[Lombok] lombok 라이브러리는 import 되는데 어노테이션이 적용되지 않을 때 에러 해결 (SpringToolSuite4 오류) (0) | 2024.04.27 |
---|---|
[Spring boot Project] 조회 페이지와 영화 리뷰 (0) | 2023.09.27 |
[Spring Boot Project] Ajax로 파일 업로드하고 데이터베이스에 저장하기 (2) | 2023.09.22 |
[Spring boot Project] 업로드 파일 삭제 (0) | 2023.09.20 |
[Spring boot Project] 썸네일 이미지 생성과 화면 처리 (0) | 2023.09.20 |