📚 공부한 책 : 코드로배우는 스프링 부트 웹프로젝트
❤️ github 주소 : https://github.com/qkralswl689/LearnFromCode/tree/main/guestbook2022
구성요소
- 시작 페이지 번호
- 끝 페이지 번호
- 이전/다음 이동 링크 여부
- 현재 페이지 번호
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;
// 화면에서 필요한 결과는 PageResultDTO라는 이름으로 생성한다
@Data
// 다양한 곳에서 사용할 수 있도록 제네릭 타입을 이용해 DTO 와 EN(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<EN,DTO> : 엔티티 객체들을 DTO로 변환해주는 기능
public PageResultDTO(Page<EN> result, Function<EN,DTO> fn){
dtoList = result.stream().map(fn).collect(Collectors.toList());
totalPage = result.getTotalPages();
makePageList(result.getPageable());
}
private void makePageList(Pageable pageable){
this.page = pageable.getPageNumber() + 1 ; // 0부터 시작하므로 1을 더해준다
this.size = pageable.getPageSize();
// temp end page
// 끝번호를 미리 계산하는 이유 : 시작번호 계산 수월하게 하기위해
// * 끝 번호 구하는 공식
int tempEnd = (int)(Math.ceil(page / 10.0)) * 10;
start = tempEnd - 9; // 화면에 10페이지씩 보여준다면 시작번호는 무조건 끝번호에서 9를 뺀 값이다
prev = start > 1;
end = totalPage > tempEnd ? tempEnd : totalPage;
next = totalPage > tempEnd;
pageList = IntStream.rangeClosed(start,end).boxed().collect(Collectors.toList());
}
}
import com.example.guestbook2022.dto.GuestbookDTO;
import com.example.guestbook2022.dto.PageRequestDTO;
import com.example.guestbook2022.dto.PageResultDTO;
import com.example.guestbook2022.entity.Guestbook;
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 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);
}*/
System.out.println(resultDTO);
}
}
Hibernate:
select
guestbook0_.gno as gno1_0_,
guestbook0_.moddate as moddate2_0_,
guestbook0_.regdate as regdate3_0_,
guestbook0_.content as content4_0_,
guestbook0_.title as title5_0_,
guestbook0_.writer as writer6_0_
from
guestbook guestbook0_
order by
guestbook0_.gno desc limit ?
Hibernate:
select
count(guestbook0_.gno) as col_0_0_
from
guestbook guestbook0_
PageResultDTO(dtoList=[GuestbookDTO(gno=302, title=등록 테스트, content=등록!, writer=userming, regDate=2022-02-01T13:09:49.781778, modDate=2022-02-01T13:09:49.781778),
GuestbookDTO(gno=301, title=Sample Title..., content=Sample Content..., writer=user0, regDate=2022-01-31T19:43:17.140328, modDate=2022-01-31T19:43:17.140328),
GuestbookDTO(gno=300, title=Changed Title..., content=Changed Content..., writer=user0, regDate=2022-01-31T14:16:14.908530, modDate=2022-01-31T14:24:46.650907),
GuestbookDTO(gno=299, title=Title...299, content=Content...299, writer=user9, regDate=2022-01-31T14:16:14.904784, modDate=2022-01-31T14:16:14.904784),
GuestbookDTO(gno=298, title=Title...298, content=Content...298, writer=user8, regDate=2022-01-31T14:16:14.900567, modDate=2022-01-31T14:16:14.900567),
GuestbookDTO(gno=297, title=Title...297, content=Content...297, writer=user7, regDate=2022-01-31T14:16:14.892919, modDate=2022-01-31T14:16:14.892919),
GuestbookDTO(gno=296, title=Title...296, content=Content...296, writer=user6, regDate=2022-01-31T14:16:14.890396, modDate=2022-01-31T14:16:14.890396),
GuestbookDTO(gno=295, title=Title...295, content=Content...295, writer=user5, regDate=2022-01-31T14:16:14.887866, modDate=2022-01-31T14:16:14.887866),
GuestbookDTO(gno=294, title=Title...294, content=Content...294, writer=user4, regDate=2022-01-31T14:16:14.885068, modDate=2022-01-31T14:16:14.885068),
GuestbookDTO(gno=293, title=Title...293, content=Content...293, writer=user3, regDate=2022-01-31T14:16:14.882828, modDate=2022-01-31T14:16:14.882828)],
totalPage=31, page=1, size=10, start=1, end=10, prev=false, next=true, pageList=[1, 2, 3, 4, 5, 6, 7, 8, 9, 10])
실제화면에 페이징 기능을 반영한다 -> 컨트롤러 작성 , thymeleaf 작성
import com.example.guestbook2022.dto.GuestbookDTO;
import com.example.guestbook2022.dto.PageRequestDTO;
import com.example.guestbook2022.service.GuestbookService;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
@Controller
@RequiredArgsConstructor // 자동주입
@RequestMapping("/guestbook")
public class GuestbookController {
private final GuestbookService service;
@GetMapping("/")
public String index(){
return "redirect:/guestbook/list";
}
@GetMapping("/list")
public void list(@ModelAttribute PageRequestDTO pageRequestDTO , Model model){
// 실제로 model에 추가되는 데이터 : PageResultDTO
// => Model을 이용해 GuestbookServiecImpl에서 반환하는 PageResultDTO를 result 라는 이름으로 전달
model.addAttribute("result", service.getList(pageRequestDTO));
}
}
- 부스트트랩의 테이블 구조를 이용하여 전달받은 dtoList 를 이용해 GuestbookDTO들을 출력한다
- th:each를 이용해 PageResultDTO 안에 있는 dtoList를 반복처리한다
- 페이지의 이전(previous)와 다음(next)부분은 if를 이용해 처리
- 현재 페이지 여부를 체크해 'active'라는 이름의 클래스가 출력되도록 작성
- 이전(previous)의 경우 PageResultDTO의 start 값에서 1적은 값으로 지정
- 다음(next)의 경우 PageResultDTO의 end 값에서 1큰 값으로 지정
<!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>gestbook test</h1>
<table class="table table-striped">
<thead>
<tr>
<th scope="col">Gno</th>
<th scope="col">Title</th>
<th scope="col">Writer</th>
<th scope="col">Regdate</th>
</tr>
</thead>
<tbody>
<!--th:each를 이용해 PageResultDTO 안에 있는 dtoList를 반복처리한다-->
<tr th:each="dto : ${result.dtoList}" >
<th scope="row">[[${dto.gno}]]</th>
<td>[[${dto.title}]]</td>
<td>[[${dto.writer}]]</td>
<td>[[${#temporals.format(dto.regDate, 'yyyy/MM/dd')}]]</td>
</tr>
</tbody>
</table>
<!--페이지의 이전(previous)와 다음(next)부분은 if를 이용해 처리-->
<ul class="pagination h-100 justify-content-center align-items-center">
<!--현재 페이지 여부를 체크해 'active'라는 이름의 클래스가 출력되도록 작성-->
<li class="page-item" th:if="${result.prev}">
<!--이전(previous)의 경우 PageResultDTO의 start 값에서 1적은 값으로 지정-->
<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>
<!--현재 페이지 여부를 체크해 'active'라는 이름의 클래스가 출력되도록 작성-->
<li class="page-item" th:if="${result.next}">
<!--다음(next)의 경우 PageResultDTO의 end 값에서 1큰 값으로 지정-->
<a class="page-link" th:href="@{/guestbook/list(page = ${result.end +1})}">Next</a>
</li>
</ul>
</th:block>
</th:block>
게시글 등록페이지 구현
- 등록 : GET 방식 -> 화면을 보여준다 , POST방식 -> 처리 후 목록 페이지로 이동
- RedirectAttributes() : 한 번만 화면에서 변수를 사용할 수 있도록 처리
- addFlashAttribute() : 단 한번만 데이터를 전달하는 용도로 사용한다
import com.example.guestbook2022.dto.GuestbookDTO;
import com.example.guestbook2022.dto.PageRequestDTO;
import com.example.guestbook2022.service.GuestbookService;
import lombok.RequiredArgsConstructor;
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
@RequiredArgsConstructor // 자동주입
@RequestMapping("/guestbook")
public class GuestbookController {
private final GuestbookService service;
// 화면을 보여준다
@GetMapping("/register")
public void register(){
}
// 처리 후 목록 페이지로 이동
@PostMapping("/register")
public String registerPost(GuestbookDTO dto, RedirectAttributes redirectAttributes){
// 새로 추가된 엔티티의 번호
Long gno = service.register(dto);
// addFlashAttribute() : 단 한번만 데이터를 전달하는 용도로 사용한다
// redirectAttributes : 한 번만 화면에서 "msg"라는 이름의 변수를 사용할 수 있도록 처리
redirectAttributes.addFlashAttribute("msg", gno);
return "redirect:/guestbook/list";
}
}
from 태그에는 action속성값을 "/guestbook/register"로 지정하고 POST방식으로 전송할 수 있도록 한다
<!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>
<!--action속성값을 "/guestbook/register"로 지정하고 POST방식으로 전송할 수 있도록 한다-->
<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>
게시글 등록 처리 후 처리된 결과를 모달창으로 보여준다
- JavaScript를 이용해 등록 후 전달되는 msg 값을 확인한다
- 등록후 msg 변수에 새로 생성된 글의 번호가 할당 -> msg 변수의 값을 이용해 모달창을 실행한다
<!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>gestbook test</h1>
<table class="table table-striped">
<thead>
<tr>
<th scope="col">Gno</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">[[${dto.gno}]]</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">×</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>
<script th:inline="javascript">
<!--등록 후 전달되는 msg 값을 확인한다-->
var msg = [[${msg}]];
console.log(msg);
<!--등록후 msg 변수에 새로 생성된 글의 번호가 할당 -> 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">
<!-- 그외의 코드 생략 -->
<!--추가-->
<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>
<!-- 그외의 코드 생략 -->
</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>gestbook test
<span>
<!--추가-->
<a th:href="@{/guestbook/register}">
<button type="button" class="btn btn-outline-primary">REGISTER
</button>
</a>
</span>
</h1>
<!-- 그외의 코드 생략 -->
</th:block>
</th:block>