1. 프로젝트 구조 생성
- Spring Starter Project로 프로젝트 생성하고 라이브러리 생성
2. pom.xml에 라이브러리 추가
- Add를 눌러 라이브러리를 추가한다.(querydsl-apt, querydsl-jpa, thymeleaf-layout-dialect)
<plugin>
<groupId>com.mysema.maven</groupId>
<artifactId>apt-maven-plugin</artifactId>
<version>1.1.3</version>
<executions>
<execution>
<goals>
<goal>process</goal>
</goals>
<configuration>
<outputDirectory>target/generated-sources/java</outputDirectory>
<processor>com.querydsl.apt.jpa.JPAAnnotationProcessor</processor>
</configuration>
</execution>
</executions>
</plugin>
- build쪽에 다음 구문을 추가한다.(어노테이션 관련 구문)
- properties로 들어가서 Annotation Processing을 한다.
3. application.properties에 구문을 삽입한다. 참고로 이전 Spring게시물과 똑같다.
4. templates폴더에 layout 폴더 생성 후 boards, layout 파일들 장입하기
- 해당 파일들을 아래의 깃허브에서 다운 받고 boards, layout, static 파일들을 붙여넣습니다.
https://github.com/zerockcode/boot07
GitHub - zerockcode/boot07: 스타트 스프링 부트 7장
스타트 스프링 부트 7장. Contribute to zerockcode/boot07 development by creating an account on GitHub.
github.com
(참고로 list.html은 코팅을 하던 도중 오류가 생겨서 아래에 있는 구문을 대체했습니다.)
대체 list.html 구문
<html xmlns:th="http://www.thymeleaf.org"
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
layout:decorate="~{/layout/layout1}">
<div layout:fragment="content">
<div class="panel-heading">List Page</div>
<div class="panel-body pull-right">
<h3><a class="label label-default " th:href="@{register}">Register</a></h3>
</div>
<div class="panel-body">
<div th:with="result=${result.result}">
<table class="table table-striped table-bordered table-hover"
id="dataTables-example">
<thead>
<tr>
<th>BNO</th>
<th>TITLE</th>
<th>WRITER</th>
<th>REGDATE</th>
</tr>
</thead>
<tbody>
<tr class="odd gradeX" th:each="board:${result.content}">
<td>[[${board.bno}]]</td>
<td><a th:href='${board.bno}' class='boardLink'>[[${board.title}]]</a></td>
<td>[[${board.writer}]]</td>
<td class="center">[[${#dates.format(board.regdate,
'yyyy-MM-dd')}]]</td>
</tr>
</tbody>
</table>
<div>
<select id='searchType'>
<option>--</option>
<option value='t' th:selected="${pageVO.type} =='t'" >Title</option>
<option value='c' th:selected="${pageVO.type} =='c'">Content</option>
<option value='w' th:selected="${pageVO.type} =='w'">Writer</option>
</select>
<input type='text' id='searchKeyword' th:value="${pageVO.keyword}">
<button id='searchBtn'>Search</button>
</div>
</div>
<nav>
<div>
<ul class="pagination">
<li class="page-item" th:if="${result.prevPage}"><a
th:href="${result.prevPage.pageNumber} + 1">PREV
[[${result.prevPage.pageNumber} + 1]]</a></li>
<li class="page-item"
th:classappend="${p.pageNumber == result.currentPageNum -1}?active: '' "
th:each="p:${result.pageList}"><a
th:href="${p.pageNumber} +1">[[${p.pageNumber} +1]]</a></li>
<li class="page-item" th:if="${result.nextPage}"><a
th:href="${result.nextPage.pageNumber} + 1">NEXT
[[${result.nextPage.pageNumber} + 1]]</a></li>
</ul>
</div>
</nav>
</div>
<form id='f1' th:action="@{list}" method="get">
<input type='hidden' name='page' th:value=${result.currentPageNum}>
<input type='hidden' name='size' th:value=${result.currentPage.pageSize}>
<input type='hidden' name='type' th:value=${pageVO.type}>
<input type='hidden' name='keyword' th:value=${pageVO.keyword}>
</form>
</div>
<!-- end fragment -->
<th:block layout:fragment="script">
<script th:inline="javascript">
$(window).load(function(){
var msg = [[${msg}]];
if(msg =='success') {
alert("정상적으로 처리되었습니다.");
var stateObj = { msg: "" };
}
});
$(document).ready(function() {
var formObj = $("#f1");
$(".pagination a").click(function(e) {
e.preventDefault();
formObj.find('[name="page"]').val($(this).attr('href'));
formObj.submit();
});
$(".boardLink").click(function(e){
e.preventDefault();
var boardNo = $(this).attr("href");
formObj.attr("action",[[@{'/boards/view'}]]);
formObj.append("<input type='hidden' name='bno' value='" + boardNo +"'>" );
formObj.submit();
});
$("#searchBtn").click(function(e){
var typeStr = $("#searchType").find(":selected").val();
var keywordStr = $("#searchKeyword").val();
console.log(typeStr, "" , keywordStr);
formObj.find("[name='type']").val(typeStr);
formObj.find("[name='keyword']").val(keywordStr);
formObj.find("[name='page']").val("1");
formObj.submit();
});
});
</script>
</th:block>
5. 컨트롤러 생성
package org.zerock.controller;
import java.util.stream.IntStream;
import javax.annotation.PostConstruct;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
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;
import org.zerock.domain.WebBoard;
import org.zerock.persistence.WebBoardRepository;
import org.zerock.vo.PageMaker;
import org.zerock.vo.PageVO;
@Controller
@RequestMapping("/boards")
public class WebBoardController {
@Autowired
private WebBoardRepository repo;
@PostConstruct
public void init() {
IntStream.range(0, 300).forEach(i->{
WebBoard board = new WebBoard();
board.setTitle("Sample Board Title "+i);
board.setContent("Content Sample ..."+i+" of Board");
board.setWriter("user0"+(i%10));
repo.save(board);
});
}
@GetMapping("/list") //list.html 보여주기
public String list(PageVO vo, Model model) {
Pageable page = vo.makePageable(0, "bno");
Page<WebBoard> result = repo.findAll(repo.makePredicate(vo.getType(), vo.getKeyword()), page);
model.addAttribute("result", new PageMaker<>(result));
return "boards/list";
}
@GetMapping("/register") //게시물 등록
public String registerGET(@ModelAttribute("vo")WebBoard vo) {
return "/boards/register";
}
@PostMapping("/register")
public String registerPOST(@ModelAttribute("vo")WebBoard vo, RedirectAttributes rttr){
repo.save(vo); //repository에 내용 저장
rttr.addFlashAttribute("msg", "success");
return "redirect:/boards/list";
}
@GetMapping("/view") //게시글 내용 보기
public String view(Long bno, @ModelAttribute("pageVO") PageVO vo, Model model) {
repo.findById(bno).ifPresent(board -> model.addAttribute("vo", board));
return "boards/view";
}
@GetMapping("/modify")
public String modify(Long bno, @ModelAttribute("pageVO") PageVO vo, Model model) {
repo.findById(bno).ifPresent(board->model.addAttribute("vo", board));
return "boards/modify";
}
@PostMapping("/modify") // 게시글 내용 수정
public String modifyPost(WebBoard board, PageVO vo, RedirectAttributes rttr) {
repo.findById(board.getBno()).ifPresent(origin->{
origin.setTitle(board.getTitle());
origin.setContent(board.getContent());
repo.save(origin);
rttr.addFlashAttribute("msg", "success");
rttr.addAttribute("bno", origin.getBno());
});
rttr.addAttribute("page", vo.getPage());
rttr.addAttribute("size", vo.getSize());
rttr.addAttribute("type", vo.getType());
rttr.addAttribute("keyword", vo.getKeyword());
return "redirect:/boards/view";
}
@PostMapping("/delete") //게시글 삭제
public String delete(Long bno, PageVO vo, RedirectAttributes rttr) {
repo.deleteById(bno);
rttr.addFlashAttribute("msg", "success");
rttr.addAttribute("page", vo.getPage());
rttr.addAttribute("size", vo.getSize());
rttr.addAttribute("type", vo.getType());
rttr.addAttribute("keyword", vo.getKeyword());
return "redirect:/boards/list";
}
}
- GET 방식을 이용해서 입력화면을 보고, POST방식으로 새로운 게시물을 등록하도록 한다. 이는 게시글 수정과 비슷하다.
6. 엔티티 클래스와 Repository 설계
- 엔티티 클래스 설계
package org.zerock.domain;
import java.sql.Timestamp;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.Table;
import org.hibernate.annotations.CreationTimestamp;
import org.hibernate.annotations.UpdateTimestamp;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
@Getter
@Setter
@Entity
@Table(name="tbl_webboards")
@EqualsAndHashCode(of="bno")
@ToString
public class WebBoard {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long bno;
private String title;
private String writer;
private String content;
@CreationTimestamp
private Timestamp regdate;
@UpdateTimestamp
private Timestamp updatedate;
}
- Repository 설계
package org.zerock.persistence;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.querydsl.QuerydslPredicateExecutor;
import org.zerock.domain.QWebBoard;
import org.zerock.domain.WebBoard;
import com.querydsl.core.BooleanBuilder;
import com.querydsl.core.types.Predicate;
public interface WebBoardRepository extends JpaRepository<WebBoard, Long>, QuerydslPredicateExecutor<WebBoard> {
public default Predicate makePredicate(String type, String keyword) {
BooleanBuilder builder = new BooleanBuilder();
QWebBoard board = QWebBoard.webBoard;
builder.and(board.bno.gt(0));
if(type == null) {
return builder;
}
switch (type) {
case "t":
builder.and(board.title.like("%"+keyword+"%"));
break;
case "c":
builder.and(board.content.like("%"+keyword+"%"));
break;
case "w":
builder.and(board.writer.like("%"+keyword+"%"));
break;
}
return builder;
}
}
- makePredicate()는 파라미터로 전달되는 문자열을 이용해서 switch를 처리하는 구조
7. PAGEVO 생성
package org.zerock.vo;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
public class PageVO {
private static final int DEFAULT_SIZE=10;
private static final int DEFAULT_MAX_SIZE=50;
private int page;
private int size;
private String keyword;
private String type;
public String getKeyword() {
return keyword;
}
public void setKeyword(String keyword) {
this.keyword = keyword;
}
public String getType() {
return type;
}
public void setType(String type) {
this.type = type;
}
public PageVO() {
this.page=1;
this.size=DEFAULT_SIZE;
}
public int getPage() {
return page;
}
public void setPage(int page) {
this.page = page < 0 ? 1 : page;
}
public int getSize() {
return size;
}
public void setSize(int size) {
this.size = size < DEFAULT_SIZE || size> DEFAULT_MAX_SIZE ? DEFAULT_SIZE : size;
}
public Pageable makePageable(int direction, String... props) {
Sort.Direction dir = direction == 0 ? Sort.Direction.DESC : Sort.Direction.ASC;
return PageRequest.of(this.page - 1, this.size, dir, props);
}
}
- PageVO는 브라우저에서 전달되는 값은 페이지 번호와 게시물의 수만을 받도록 설계하고, 이떄에도 일정 이상의 값이 들어올 수 없도록 제약을 둔다.
- makePageable() 메소드는 전달되는 파라미터를 이용해서 최종적으로 PageRequest로 Pageable 객체를 만들어낸다.
8. 페이지 번호 출력
package org.zerock.vo;
import java.util.ArrayList;
import java.util.List;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import lombok.Getter;
import lombok.ToString;
@Getter
@ToString(exclude = "pageList")
public class PageMaker<T> {
private Page<T> result;
private Pageable prevPage;
private Pageable nextPage;
private int currentPageNum;
private int totalPageNum;
private Pageable currentPage;
private List<Pageable> pageList;
public PageMaker(Page<T> result) {
this.result=result;
this.currentPage=result.getPageable();
this.currentPageNum=currentPage.getPageNumber()+1;
this.totalPageNum=result.getTotalPages();
this.pageList=new ArrayList<>();
calcPages();
}
private void calcPages() {
int tempEndNum = (int)(Math.ceil(this.currentPageNum/10.0)*10);
int startNum = tempEndNum -9;
Pageable startPage = this.currentPage;
for (int i = startNum; i < this.currentPageNum; i++) {
startPage = startPage.previousOrFirst();
}
this.prevPage = startPage.getPageNumber() <= 0? null :startPage.previousOrFirst();
if (this.totalPageNum < tempEndNum) {
tempEndNum=this.totalPageNum;
this.nextPage=null;
}
for (int i = startNum; i <= tempEndNum; i++) {
pageList.add(startPage);
startPage=startPage.next();
}
this.nextPage=startPage.getPageNumber()+1 < totalPageNum ? startPage:null;
}
}
- 페이지 번호 출력에 필요한 정보들을 처리하도록 한다.
9. 결과
브라우저에 http://localhost:8080/boards/list라고 입력한다.
- 첫 메인 화면이다.
- 아래에 있는 Modify/Delete를 누르면 아래 사진처럼 변하면서 내용을 수정할 수 있게 된다.
- 내용을 변경하고 Modify를 누르면 수정이 된다.
- register화면이다. register는 누르고 내용을 삽입하고 Submit Button을 누르면 게시물이 올라간다.
- Test2 게시물에서 Delete 버튼을 누르면 게시물이 삭제된다.
- 검색창에 Title로 맞추고 290이라고 치면 290이 해당된 게시물로 가게된다.
- 아래 번호가 있는데 해당 번호를 클릭하면 해당 페이지에 있는 게시물들로 이동한다.
'Spring_boot > Project' 카테고리의 다른 글
[Spring] 스프링부트(Spring Tool suite) 설치 방법 (0) | 2021.08.22 |
---|---|
[Spring] REST 방식의 댓글 처리와 JPA 처리 (0) | 2021.08.19 |
[Spring] Spring MVC를 이용한 통합 (0) | 2021.08.10 |
[Spring] Thymeleaf 동작 확인 (0) | 2021.08.08 |
[Spring] Thymeleaf 사용하기 (0) | 2021.08.07 |