Spring_boot/Project

[Spring] 웹 애플리케이션 제작

달의요정루나 2021. 8. 16. 23:43

1. 프로젝트 구조 생성

- Spring Starter Project로 프로젝트 생성하고 라이브러리 생성

필요 라이브러리

2. pom.xml에 라이브러리 추가

Dependencies로 이동

- 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 파일들 장입하기

templates에 공간 만들기
파일 집어넣기

- 해당 파일들을 아래의 깃허브에서 다운 받고 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이 해당된 게시물로 가게된다.

- 아래 번호가 있는데 해당 번호를 클릭하면 해당 페이지에 있는 게시물들로 이동한다.