2021. 10. 17. 16:45ㆍJAVA/Spring
이전글
https://yonghwankim-dev.tistory.com/147
개요
이전글에서는 게시판의 페이지 번호를 추가하고 처리하는 기능을 구현하였습니다. 이번글에서는 게시판의 검색 조건을 추가하고 검색을 하는 기능을 구현하고 게시물을 등록하는 기능을 구현하겠습니다.
1. 검색 조건 처리하기
검색 조건의 처리는 웹 페이지에서 검색 조건(type)를 선택하고 키워드(keyword)을 입력 후 검색 버튼을 누르면 조건에 맞는 게시물을 필터링하는 기능입니다. 그러기 위해서는 서버에서 타입(type)과 키워드(keyword)를 받기 위해서 PageVO 클래스를 수정해야 합니다.
org.zerock.vo.PageVO.java
private String keyword;
private String type;
package org.zerock.vo;
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 PageVO() {
this.page = 1;
this.size = DEFAULT_SIZE;
}
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 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 클래스에 type과 keyword가 인스턴스 변수로 추가되었고, getter/setter를 처리해주었습니다.
WebBoardController의 list()는 Predicate를 생성하는 부분을 전달받은 PageVO를 이용하도록 수정합니다.
org.zerock.controller.WebBoardController.java의 list()
@GetMapping("/list")
public PageMaker<WebBoard> list(PageVO vo, Model model) {
Pageable page = vo.makePageable(0, "bno");
Page<WebBoard> result = repo.findAll(repo.makePredicate(vo.getType(), vo.getKeyword()), page);
log.info(""+page);
log.info(""+result);
return new PageMaker<WebBoard>(result);
}
웹페이지에서의 게시물 검색 처리
서버에 타입(type)과 키워드(keyword)를 전달하기 위해서 다음과 같이 수정합니다.
webBoard.WebBoardListComponent.jsx의 일부
class WebBoardListComponent extends Component{
constructor(props){
super(props);
this.state = {
result : null,
prevPage : null,
nextPage : null,
pageList : [],
boards : [],
page : 1,
size : 10,
type : "",
keyword : "",
}
}
...
onChangeType = (e)=>{
this.setState({type : e.target.value});
}
onChangeKeyword = (e)=>{
this.setState({keyword : e.target.value});
}
onClickSearch = ()=>{
this.reloadWebBoardList(this.state.page, this.state.size, this.state.type, this.state.keyword);
}
render(){
return(
... 생략
{/* search */}
<div>
<select name="searchType" onChange={this.onChangeType}>
<option value="">--</option>
<option value='t'>Title</option>
<option value='w'>Writer</option>
<option value='c'>Content</option>
</select>
<input type="text" onChange={this.onChangeKeyword}/>
<button onClick={this.onClickSearch}>Search</button>
</div>
</div>
위와 같이 구현하면 게시판의 게시물이 필터링이 수행되고 필터링된 상태에서의 페이지 처리도 됩니다. 그리고 필터링된 결과의 페이지간의 이동시 초기화되지 않고 필터링 결과 내에서 이동이 수행됩니다.
에를 들어 감색 조건을 'Title', 키워드 내용을 '10'으로 입력했을시 결과는 아래와 같습니다.
before
after
위 결과를 통해서 Search 버튼을 눌렀음에도 검색 조건(Title)과 키워드(Keyword) 내용이 유지된 것을 확인할 수 있고 위 상태에서 페이지 2번 번호를 클릭하였을때 결과는 아래와 같습니다.
위 테스트 결과를 통하여 2번 페이지 번호를 클릭하였을때 게시물이 처음으로 초기화되어 출력되지 않고 필터링된 결과 내에서 페이징 처리된 것을 확인할 수 있습니다.
2. 새로운 게시물 등록
게시물의 등록 작업은 기존에 작성된 엔티티 클래스(WebBoard)를 그대로 이용할 것인지, 별도의 Value Object용 클래스를 작업해서 처리할 것인지 고민이 필요합니다. 여기서는 기존 엔티티 클래스인 org.zerock.domain.WebBoard 클래스를 그대로 이용해서 작성합니다.
2.1 게시물의 입력과 처리
우선 게시물을 입력하기 위한 입력창이 필요합니다.
src/component/webBoard/WebBoardRegisterComponent.jsx
import { Component } from "react";
import ApiService from "../../ApiService";
class WebBoardRegisterComponent extends Component{
constructor(props){
super(props);
this.state={
title : "",
content : "",
writer : ""
}
}
onChangeTitle = (e)=>{
this.setState({title : e.target.value});
}
onChangeContent = (e)=>{
this.setState({content : e.target.value});
}
onChangeWriter = (e)=>{
this.setState({writer : e.target.value});
}
onClickReset = ()=>{
this.setState({
title : "",
content : "",
writer : ""
});
}
onClickSubmit = ()=>{
ApiService.registerWebBoard(this.state.title, this.state.content, this.state.writer)
.then(res=>{
if(res.data==="success")
{
alert("정상적으로 등록되었습니다.");
this.props.history.push("/boards/list");
}
else
{
alert("등록이 실패하였습니다.");
}
})
.catch(err=>{
console.log("registerWebBoard() error!",err);
})
}
render(){
return (
<>
<div>Register Page</div>
<div>
<div>
<label>Title</label>
<input name="title" onChange={this.onChangeTitle} value={this.state.title}/>
</div>
<div>
<label>Content</label>
<textarea rows="3" name="content" onChange={this.onChangeContent} value={this.state.content}></textarea>
</div>
<div>
<label>Writer</label>
<input name="writer" onChange={this.onChangeWriter} value={this.state.writer}/>
</div>
<button onClick={()=>{this.onClickSubmit()}}>Submit Button</button>
<button onClick={()=>{this.onClickReset()}}>Reset Button</button>
</div>
</>
);
}
}
export default WebBoardRegisterComponent;
입력창으로 이동하기 위해서 WebBoardListComponent.jsx 일부를 변경합니다.
src/component/webBoard/WebBoardListComponent.jsx 일부 수정
onClickRegister = ()=>{
this.props.history.push("/boards/register");
}
render(){
return(
<>
<div>
<h2>WebBoard List</h2>
<div>
<button onClick={()=>{this.onClickRegister()}}>Register</button>
</div>
...
서버로 입력 title, content, writer 값을 보내기 위해서 ApiService.js를 수정합니다.
src/ApiService.js
import axios from 'axios';
const BOARD_API_BASE_URL = "http://localhost:8080/boards";
class ApiService{
fetchWebBoards(page, size, type, keyword){
return axios.get(BOARD_API_BASE_URL + "/list?page="+page+"&size="+size+"&type="+type+"&keyword="+keyword);
}
registerWebBoard(title, content, writer){
return axios.post(BOARD_API_BASE_URL + "/register",null,{params : {title : title, content : content, writer : writer}});
}
}
export default new ApiService();
클라이언트가 보내는 값을 받기 위해서 게시물 등록 메서드를 추가합니다.
org.zerock.controller.WebBoardController.java
@CrossOrigin(origins = "*", maxAge = 3600)
@RestController
@RequestMapping("/boards/")
@Log
public class WebBoardController {
...
@PostMapping("/register")
public String registerPOST(WebBoard vo) {
log.info("register post");
log.info("" + vo);
if(vo.getTitle().equals("") || vo.getWriter().equals(""))
{
return "fail";
}
else
{
repo.save(vo);
return "success";
}
}
}
만약 제목이나 작성자가 공백이면 클라이언트에게 "fail" 메시지를 보내고 아닌 경우 게시물을 저장하고 "success" 메시지를 전송합니다.
실행확인
등록이 완료되면 알림문을 받고 기존 게시물 목록 화면으로 이동합니다.
전체코드
src/component/webBoard/WebBoardListComponent.jsx
import React, {Component} from 'react';
import ApiService from "../../ApiService";
import queryStirng from 'query-string';
class WebBoardListComponent extends Component{
constructor(props){
super(props);
this.state = {
result : null,
prevPage : null,
nextPage : null,
pageList : [],
boards : [],
page : 1,
size : 10,
type : "",
keyword : "",
}
}
componentDidMount(){
this.reloadWebBoardList(this.state.page, this.state.size, this.state.type, this.state.keyword);
}
reloadWebBoardList = (page=1, size=10, type="", keyword="")=>{
ApiService.fetchWebBoards(page, size, type, keyword)
.then(res=>{
this.setState({
result : res.data,
prevPage : res.data.prevPage,
nextPage : res.data.nextPage,
pageList : res.data.pageList,
boards:res.data.result.content
})
})
.catch(err=>{console.log("reloadWebBoardList() Error!",err);});
}
onChangePage = (p)=>{
this.setState({page : p});
this.reloadWebBoardList(p,this.state.size, this.state.type, this.state.keyword);
}
onChangeType = (e)=>{
this.setState({type : e.target.value});
}
onChangeKeyword = (e)=>{
this.setState({keyword : e.target.value});
}
onClickSearch = ()=>{
this.reloadWebBoardList(this.state.page, this.state.size, this.state.type, this.state.keyword);
}
onClickRegister = ()=>{
this.props.history.push("/boards/register");
}
render(){
return(
<>
<div>
<h2>WebBoard List</h2>
<div>
<button onClick={()=>{this.onClickRegister()}}>Register</button>
</div>
<table>
<thead>
<tr>
<th>Bno</th>
<th>Title</th>
<th>Wirter</th>
<th>Content</th>
<th>Regdate</th>
<th>Updatedate</th>
</tr>
</thead>
<tbody>
{
this.state.boards.map( board=>
<tr key={board.bno}>
<td>{board.bno}</td>
<td>{board.title}</td>
<td>{board.writer}</td>
<td>{board.content}</td>
<td>{board.regdate}</td>
<td>{board.updatedate}</td>
</tr>
)
}
</tbody>
</table>
{/* search */}
<div>
<select name="searchType" onChange={this.onChangeType}>
<option value="">--</option>
<option value='t'>Title</option>
<option value='w'>Writer</option>
<option value='c'>Content</option>
</select>
<input type="text" onChange={this.onChangeKeyword}/>
<button onClick={this.onClickSearch}>Search</button>
</div>
</div>
<div>
{/* pagination */}
<ul>
{
/* PREV 버튼 */
this.state.prevPage===null ? null : <li><button onClick={()=>{this.onChangePage(this.state.prevPage.pageNumber+1)}}>PREV {this.state.prevPage.pageNumber+1}</button></li>
}
{
/* 페이지 번호 버튼 */
this.state.pageList.map(page =>
<li key={page.pageNumber+1}>
{
this.state.result.currentPageNum-1===page.pageNumber ?
<button onClick={()=>{this.onChangePage(page.pageNumber+1)}} style={{color:"red"}}>{page.pageNumber+1}</button>
:
<button onClick={()=>{this.onChangePage(page.pageNumber+1)}}>{page.pageNumber+1}</button>
}
</li>
)
}
{
/* NEXT 버튼 */
this.state.nextPage===null ? null : <li><button onClick={()=>{this.onChangePage(this.state.nextPage.pageNumber+1)}}>NEXT {this.state.nextPage.pageNumber+1}</button></li>
}
</ul>
</div>
</>
);
}
}
export default WebBoardListComponent;
org.zerock.controller.WebBoardController.java
package org.zerock.controller;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.CrossOrigin;
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.bind.annotation.RestController;
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;
import lombok.extern.java.Log;
@CrossOrigin(origins = "*", maxAge = 3600)
@RestController
@RequestMapping("/boards/")
@Log
public class WebBoardController {
@Autowired
private WebBoardRepository repo;
@GetMapping("/list")
public PageMaker<WebBoard> list(PageVO vo, Model model) {
Pageable page = vo.makePageable(0, "bno");
Page<WebBoard> result = repo.findAll(repo.makePredicate(vo.getType(), vo.getKeyword()), page);
log.info(""+page);
log.info(""+result);
return new PageMaker<WebBoard>(result);
}
@PostMapping("/register")
public String registerPOST(WebBoard vo) {
log.info("register post");
log.info("" + vo);
if(vo.getTitle().equals("") || vo.getWriter().equals(""))
{
return "fail";
}
else
{
repo.save(vo);
return "success";
}
}
}
더 자세한 전체 소스코드는 아래 source code에서 boot06 프로젝트에서 확인이 가능합니다.
References
source code : https://github.com/yonghwankim-dev/SpringBoot-Study
스타트 스프링 부트, 구멍가게코딩단 지음