본문 바로가기

개발 공부/Spring

코드로 배우는 스프링 웹 프로젝트-15.검색 처리

코드로 배우는 스프링 웹 프로젝트-15.검색 처리

코드로 배우는 스프링 웹 프로젝트 - 개정판

2019년 7월 10일 인쇄판

 

Part3. 기본적인 웹 게시물 관리

Chapter15. 검색 처리

 

검색 기능은 검색 조건과 키워드로 나눠 생각해볼 수 있다.

검색 조건은 일반적으로 select 태그나 checkbox를 이용한다. 최근에는 select를 일반 사용자들의 경우에, 관리자용이나 검색 기능이 강한 경우 checkbox를 이용하는 경우가 대부분이다.

 

 

15.1 검색 기능과 SQL

 

- 제목/내용/작성자와 같이 단일 항목 검색

- 제목/내용 or 작성자/내용 등 다중 항목 검색

 

단일 항목은 인라인뷰 안쪽에 필요한 데이터를 가져올 때 검색 조건이 적용되어야 하기 때문에 WEHRE 문 뒤에 검색 조건이 추가되고 ROWNUM 조건이 뒤따르게 하면 문제없다.

 

15.1.1 다중 항목 검색

 

select * from
(
select /*+INDEX_DES(tbl_board po_board) */
 rownum rn, bno, title, content, writer, regdate, updatedate
 from tbl_board
 where title like '%Test%' or content like '%Test%'
 	and rownum <=20
)
where rn>10;

위 SQL의 경우, 10개의 데이터가 아니라 많은 양의 데이터가 나온다.

AND 연산자가 OR 연산자보다 우선순위가 높기 때문에 ROWNUM이 20보다 작거나 같으면서 (AND) 내용에 Test라는 문자열이 있거나 제목에 Test라는 문자열이 있는 게시물을 검색하게 된다.

 

AND, OR이 섞어 있는 경우 그래서 ()를 적절히 쳐주는 것이 중요하다.

 

15.2 MyBatis의 동적 SQL

 

검색 조건이 변하면 SQL문 역시 변하므로 XML이나 어노테이션과 같이 고정된 문자열을 작성하는 방식으론 제대로 처리할 수 없다. 다행히 MyBatis는 동적 태그 기능을 이용해 SQL을 파라미터의 조건에 맞게 조정할 수 있는 기능을 제공한다

 

 

15.2.1 MyBatis의 동적 태그들

 

- if

- choose(when, otherwise)

- trim(where, set)

- foreach

 

<if>

 

- 검색 조건이 T면 제목이 키워드인 항목을 검색

- 조건이 C면 내용이 키워드인 항목 검색

- 조건이 W면 작성자가 키워드인 항목 검색

 

<if test="type=='T'.toString()">
	(title like '%' || #{keyword} || '%')
</if>
<if test="type=='C'.toString()">
	(content like '%' || #{keyword} || '%')
</if>
<if test="type=='W'.toString()">
	(writer like '%' || #{keyword} || '%')
</if>

 

if 안에 들어가는 표현식은 OGNL 표현식이다.

자세한 내용은 https://commons.apache.org/proper/commons-ognl/language-guide.html 참고

 

 

<choose>

 

<choose>
<when test="type=='T'.toString()">
	(title like '%' || #{keyword} || '%')
</when>
<when test="type=='T'.toString()">
	(title like '%' || #{keyword} || '%')
</when>
<when test="type=='T'.toString()">
	(title like '%' || #{keyword} || '%')
</when>
<otherwise>
	(title like '%' || #{keyword} || '%' OR content like '%' || #{keyword} || '%')
</otherwise>
</choose>

 

<trim>, <where>, <set>

 

if, choose 태그 안에서 SQL문을 연결해 주고, 앞 뒤에 필요한 구문들을 추가하거나 생략하는 역할을 한다.

태그 안쪽에서 SQL이 생성되면 WHERE 구문이 붙고, 그렇지 않은 경우 생성되지 않는다.

 

select * from tbl_board
<where>
	<if test="bno !=null">
    bno = #{bno}
    </if>
</where>

 

<trim>은 태그의 내용을 앞의 내용과 관련되어 원하는 접두/접미를 처리할 수 있다.

 

select * from tbl_board
<where>
	<if test="bno!=null">
    bno=#{bno}
    </if>
    <trim prefixOverrides ="and">
    rownum=1
    </trim>
</where>

 

trim은 prefix, suffix, prefixOverrides, suffixOverrides 속성 지정 가능.

 

 

<foreach>

 

List, 배열, 맵 등을 이용해 루프 처리 가능. 주로 IN 조건에서 많이 사용

예를 들어 제목은 TTTT로, 내용을 CCCC 값으로 이용한다면

 

Map<String, STring> map= new HashMap<>();
map.put("T", "TTTT");
map.put("C", "CCCC");

작성된 Map을 파라미터로 전달하고 foreach를 이용한다.

 

select * from tbl_board
  <trim prefix="where (" suffix=")" prefixOverrides="OR">
  	<foreach item="val" index="key" collection="map">
    
    	<trim prefix="OR">
        	<if test="key == 'C'.toString()">
            	content = #{val}
            </if>
        	<if test="key == 'T'.toString()">
            	title = #{val}
            </if>
            <if test="key == 'W'.toString()">
           		writer = #{val}
            </if>
        </trim>
    </foreach>
</trim>

foreach를 배열이나 List를 이용해야 하는 경우 item 속성만을 이용하면 되고 MAp의 형태로 key와 value를 이용해야 하면 index와 item 속성을 둘 다 이용한다.

 

 

15.3 검색 조건 처리를 위한 Criteria의 변화

 

검색 조건을 처리하기 위해 검색 조건과 검색에 사용하는 키워드가 필요하므로 기존의 Criteria를 확장할 필요가 있다.

 

package org.zerock.domain;

import lombok.Getter;
import lombok.Setter;
import lombok.ToString;

@Getter
@Setter
@ToString
public class Criteria {
	private int pageNum;
	private int amount;
	
	private String type;
	private String keyword;
	
	public Criteria() {
		this(1, 10);
	}

	
	public Criteria(int pageNum, int amount) {
		this.pageNum = pageNum;
		this.amount = amount;
	}
	
	public String[] getTypeArr() {
		return type == null? new String[] {} : type.split("");
	}
	
}

 

type과 keyword 변수 추가. getTypeArr은 검색 조건이 T, W, C로 구성되어 있을 때 검색 조건을 배열로 만들어 한 번에 처리하기 위함이다.

 

15.3.1 BoardMapper.xml에서 Criteria 처리

 

getListWithPaging()을 수정한다.

 

<select id="getListWithPaging" resultType="org.zerock.domain.BoardVO">
	<![CDATA[
		select
			bno, title, content, writer, regdate, updateDate
		from 
		(
 		   select /*+INDEX_DESC(tbl_board pk_board) */
 		   	rownum rn, bno, title, content, writer, regdate, updateDate
  		  from
   		 	tbl_board
   		 where 
   	]]>
   			<trim prefix="(" suffix=") AND " prefixOverrides="OR">
   				<foreach item='type' collection="typeArr">
   					<trim prefix="OR">
   						<choose>
   							<when test="type == 'T'.toSTring()">
   								title like '%' || #{keyword} || '%'
   							</when>
   							<when test="type == 'C'.toSTring()">
   								content like '%' || #{keyword} || '%'
   							</when>
   							<when test="type == 'W'.toSTring()">
   								writer like '%' || #{keyword} || '%'
   							</when>
   						</choose>
   					</trim>
   				</foreach>
   			</trim>
   	<![CDATA[
   		 rownum<= #{pageNum} * #{amount}
    	)
		where rn > (#{pageNum}-1) *  #{amount}
	]]>
</select>


<select id="getTotalCount" resultType="int">
	select count(*) from tbl_board where bno > 0
</select>


</mapper>

검색 조건이 총 3가지이므로 6가지 조합이 가능하지만 각 문자열을 이용해 검색 조건을 결합하는 형태로 하면 3개의 동적 SQL 구문만으로 처리를 할 수 있다.

foreach를 이용해 검색 조건을 처리하는데 typeArr 속성을 이용한다.

MyBatis는 원하는 속성을 찾을 때 getTypeArr과 같이 이름에 기반을 두어 감색하므로 Criteria에서 만들어 둔 getTypeArr 결과인 문자열 배열이 foreach 대상이 된다.

 

<choose> 안쪽의 동적 SQL은 OR title.... OR content... OR writer와 같은 구문을 만들어 내개 된다. 따라서 바깥쪽에서는 trim을 이용해 맨 앞에 생성되는 OR를 없애준다.

 

BoardMapperTests 수정

	
	@Test
	public void testSearch() {
		Criteria cri = new Criteria();
		cri.setKeyword("새로");
		cri.setType("TC");
		List<BoardVO> list = mapper.getListWithPaging(cri);
		list.forEach(board->log.info(board));
	}
	

 

<sql> <include>와 검색 데이터 개수 처리

 

동적 SQL을 이용해 검색 조건을 처리하는 부분은 해당 데이터의 개수를 처리하는 부분에서도 동일하게 적용되어야 한다.

MyBatis는 <sql> 태그로 SQL 일부를 별도 보관하고 필요한 경우 include 시키는 형태로 사용할 수 있다.

 

	<sql id="criteria">
		<trim prefix="(" suffix=") AND " prefixOverrides="OR">
			<foreach item='type' collection="typeArr">
				<trim prefix="OR">
					<choose>
						<when test="type == 'T'.toString()">
							title like '%'||#{keyword}||'%'
						</when>
						<when test="type == 'C'.toString()">
							content like '%'||#{keyword}||'%'
						</when>
						<when test="type == 'W'.toString()">
							writer like '%'||#{keyword}||'%'
						</when>
					</choose>
				</trim>
			</foreach>
		</trim>
	</sql>
	
	<select id="getListWithPaging"
		resultType="org.zerock.domain.BoardVO">
	<![CDATA[
		select
			bno, title, content, writer, regdate, updateDate
		from 
		(
 		   select /*+INDEX_DESC(tbl_board pk_board) */
 		   	rownum rn, bno, title, content, writer, regdate, updateDate
  		  from
   		 	tbl_board
   		 where 
   	]]>
	<include refid="criteria"></include>
   	<![CDATA[
   		 rownum<= #{pageNum} * #{amount}
    	)
		where rn > (#{pageNum}-1) *  #{amount}
	]]>
	</select>


	<select id="getTotalCount" resultType="int">
		select count(*) from tbl_board where 
		<include refid="criteria"> </include>
        <include refid="criteria"></include>
		 bno > 0
	</select>


</mapper>

 

 

15.4 화면에서 검색 조건 처리

 

- 페이지 번호가 파라미터로 유지되었던 것처럼 검색 조건과 키워드 역시 화면 이동 시 같이 전송되어야 한다.

- 화면에서 검색 버튼을 클릭하면 새로 검색을 한다는 의미이므로 1페이지로 이동

- 한글의 경우 GET 방식을 쓰면 문제가 생길 수 있다.

 

 

15.4.1 목록 화면에서 검색 처리

 

list.jsp 수정.

 

                       </c:forEach>
                                </tbody>
                            </table>
                            
                            <div class='row'>
                            	<div class="col-lg-12">
                            		<form id='searchForm' action="/board/list" method='get'>
                            			<select name='type'>
                            				<option value="">--</option>
                            				<option value="T">제목</option>
                            				<option value="C">내용</option>
                            				<option value="W">작성자</option>
                            				<option value="TC">제목 or 내용</option>
                            				<option value="TW">제목 or 작성자</option>
                            				<option value="TWC">제목 or 내용 or 작성자</option>
                            			</select>
                            			<input type='text' name='keyword'/>
                            			<input type='hidden' name='pageNum' value='${pageMaker.cri.pageNum }'>
                            			<input type='hidden' name='amount' value='${pageMaker.cri.amount }'>
                            			<button class='btn btn-default'>Search</button>
                            		</form>
                            	</div>
                            </div>
                            <div class='pull-right'>

HTML을 보면 페이징 처리를 위해 만들어둔 form 태그에 select와 input 태그가 추가된 것을 볼 수 있다.

 

form 내 button 기본 동작은 submit이므로 별도의 처리 없이 검색이 되는지 확인한다.

 

 

검색 버튼의 이벤트 처리

 

list.jsp 수정

 

<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>
<%@ taglib uri="http://java.sun.com/jsp/jstl/fmt" prefix="fmt" %>
<%@include file="../includes/header.jsp" %>
            <div class="row">
                <div class="col-lg-12">
                    <h1 class="page-header">Tables</h1>
                </div>
                <!-- /.col-lg-12 -->
            </div>
            <!-- /.row -->
            <div class="row">
                <div class="col-lg-12">
                    <div class="panel panel-default">
                        <div class="panel-heading">
                           Board List Page
                           <button id='regBtn' type="button" class="btn btn-xs pull-right" >
                           		Register New Board
                           </button>
                        </div>
                        <!-- /.panel-heading -->
                        <div class="panel-body">
                            <table class="table table-striped table-bordered table-hover">
                                <thead>
                                    <tr>
                                        <th>번호</th>
                                        <th>제목</th>
                                        <th>작성자</th>
                                        <th>작성일</th>
                                        <th>수정일</th>
                                    </tr>
                                </thead>
                                
                                <tbody>
                                	<c:forEach items="${list}" var="board">
                                	    <tr class="odd gradeX">
                                 	       <td>
                                 	       		<c:out value="${board.bno}"/>
                                 	       </td>
                                 	       <td>
                                 	       		<a class='move' href='<c:out value="${board.bno}"/>'>
                                 	       		  <c:out value="${board.title }"/>
                                 	       		</a>
                                 	       	</td>
                                 	       <td><c:out value="${board.writer}"/></td>
                                 	       <td><fmt:formatDate pattern="yyyy-MM-dd" value="${board.regdate }"/></td>
                                 	       <td><fmt:formatDate pattern="yyyy-MM-dd" value="${board.updateDate }"/></td>
                                 	   </tr>
                                   </c:forEach>
                                </tbody>
                            </table>
                            
                            <div class='row'>
                            	<div class="col-lg-12">
                            		<form id='searchForm' action="/board/list" method='get'>
                            			<select name='type'>
                            				<option value=""<c:out value="${pageMaker.cri.type == null?'selected':''}"/>>--</option>
                            				<option value="T"<c:out value="${pageMaker.cri.type eq 'T'?'selected':''}"/>>제목</option>
                            				<option value="C"<c:out value="${pageMaker.cri.type eq 'C'?'selected':''}"/>>내용</option>
                            				<option value="W"<c:out value="${pageMaker.cri.type eq 'W'?'selected':''}"/>>작성자</option>
                            				<option value="TC"<c:out value="${pageMaker.cri.type eq 'TC'?'selected':''}"/>>제목 or 내용</option>
                            				<option value="TW"<c:out value="${pageMaker.cri.type eq 'TW'?'selected':''}"/>>제목 or 작성자</option>
                            				<option value="TWC"<c:out value="${pageMaker.cri.type eq 'TWC'?'selected':''}"/>>제목 or 내용 or 작성자</option>
                            			</select>
                            			<input type='text' name='keyword' value='<c:out value="${pageMaker.cri.keyword }"/>'>
                            			<input type='hidden' name='pageNum' value='<c:out value="${pageMaker.cri.pageNum }"/>'>
                            			<input type='hidden' name='amount' value='<c:out value="${pageMaker.cri.amount }"/>'>
                            			<button class='btn btn-default'>Search</button>
                            		</form>
                            	</div>
                            </div>
                            <div class='pull-right'>
                            	<ul class="pagination">
                            		<c:if test="${pageMaker.prev }">
                            			<li class="paginate_button previous"><a href="${pageMaker.startPage-1 }">Previous</a>
                            			</li>
                            		</c:if>
                            		
                            		<c:forEach var="num" begin="${pageMaker.startPage }" end="${pageMaker.endPage }">
                            			<li class="paginate_button ${pageMaker.cri.pageNum==num ? "active":"" } "> 
                            				<a href="${num }">${num }</a> 
                            			</li>
                            		</c:forEach>
                            		
                            		<c:if test="${pageMaker.next }">
                            			<li class="paginate_button next"><a href="${pageMaker.endPage+1 }">Next</a>
                            			</li>
                            		</c:if>
                            	</ul>
                            	<form id='actionForm' action="/board/list" method='get'>
                            		<input type='hidden' name='pageNum' value='${pageMaker.cri.pageNum }'>
                            		<input type='hidden' name='amount' value='${pageMaker.cri.amount }'>
                            		<input type='hidden' name='type'
									value='<c:out value="${ pageMaker.cri.type }"/>'> 
									<input type='hidden' name='keyword'
									value='<c:out value="${ pageMaker.cri.keyword }"/>'>
                           		 </form>
                            </div>
                            
                            <!--  end Pagination  -->
                            <!-- Modal -->
                            <div class="modal fade" id="myModal" tabindex="-1" role="dialog" aria-labelledby="myModalLabel" aria-hidden="true">
                                <div class="modal-dialog">
                                    <div class="modal-content">
                                        <div class="modal-header">
                                            <button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
                                            <h4 class="modal-title" id="myModalLabel">Modal title</h4>
                                        </div>
                                        <div class="modal-body">
                                        	처리가 완료되었습니다.
                                        </div>
                                        <div class="modal-footer">
                                            <button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
                                            <button type="button" class="btn btn-primary">Save changes</button>
                                        </div>
                                    </div>
                                    <!-- /.modal-content -->
                                </div>
                                <!-- /.modal-dialog -->
                            </div>
                            <!-- /.modal -->
                            
                        </div>
                        <!-- /.panel-body -->
                    </div>
                    <!-- /.panel -->
                </div>
                <!-- /.col-lg-12 -->
            </div>
        
            <script type="text/javascript">
            	$(document).ready(function() {
            		var result='<c:out value="${result}"/>';
            		checkModal(result);
            		
            		history.replaceState({}, null, null);
            		
            		function checkModal(result) {
            			if(result==='' || history.state) {
            				return;
            			}
            			
            			if(parseInt(result) > 0) {
            				$(".modal-body").html("게시글 " + parseInt(result) + "번이 등록되었습니다.");
            			}
            			
            			$("#myModal").modal("show");
            		}
            		
            		$("#regBtn").on("click", function() {
            			self.location="/board/register";
            		});
            		
            		var actionForm = $("#actionForm");
            		
            		$(".paginate_button a").on("click", function(e) {
            			e.preventDefault();
            			console.log('click');
            			actionForm.find("input[name='pageNum']").val($(this).attr("href"));
            			actionForm.submit();
            		});
            		
            		$(".move").on("click", function(e) {
            			e.preventDefault();
            			actionForm.append("<input type='hidden' name='bno' value='"+$(this).attr("href")+"'>");
            			actionForm.attr("action", "/board/get");
            			actionForm.submit();
            		});
            		
            		var searchForm = $("#searchForm");
            		
            		$("#searchForm button").on("click", function(e) {
            			if(!searchForm.find("option:selected").val()) {
            				alert("검색 종류를 선택하세요");
            				return false;
            			}
            			
            			if(!searchForm.find("input[name='keyword']").val()) {
            				alert("키워드를 입력하세요");
            				return false;
            			}
            			
            			searchForm.find("input[name='pageNum']").val("1");
            			e.preventDefault();
            			
            			searchForm.submit();
            		});
            	});
            </script>

<%@include file="../includes/footer.jsp" %>
 

브라우저에서 검색 버튼을 클릭하면 form 태그 전송을 막고 페이지 번호가 1이 되도록 처리.

 

검색 부분과 form 부분 두 개, javascript 부분 수정.

 

 

15.4.2 조회 페이지에서 검색 처리

 

조회 페이지로의 이동은 이미 form 태그로 처리했으므로 별도의 처리가 필요친 않는다.

다만 Criteria의 type과 keyword를 처리해야 함.

 

get.jsp 수정

 

</div>
				<button data-oper='modify' class="btn btn-default">Modify</button>
				<button data-oper='list' class="btn btn-info">List</button>
				<form id="operForm" action="/board/modify" method="get">
					<input type='hidden' id='bno' name='bno' value='<c:out value="${board.bno}"/>' >
					<input type='hidden' name='pageNum' value='<c:out value="${cri.pageNum}"/>' >
					<input type='hidden' name='amount' value='<c:out value="${cri.amount}"/>' >
					<input type='hidden' name='keyword' value='<c:out value="${cri.keyword}"/>' >
					<input type='hidden' name='type' value='<c:out value="${cri.type}"/>' >
				</form>
			</div>
			<!-- /.panel-body -->
		</div>
		<!-- /.panel -->

15.4.3 수정/삭제 페이지에서 검색 처리

 

조회 페이지에서 수정/삭제 페이지로 이동은 GET 방식으로 이동하고, 이동 방식 역시 form 태그를 이용하는 방식이므로 기존 form 태그에 추가적인 조건만 추가.

 

modify.jsp 추가

 

			<!-- /.panel-heading -->
			<div class="panel-body">
				<form role="form" action="/board/modify" method="post">
					<input type='hidden' name='pageNum'
						value='<c:out value="${cri.pageNum }"/>'>
					<input type='hidden' name='amount' 
						value='<c:out value="${cri.amount }"/>'>
					<input type='hidden' name='keyword' value='<c:out value="${cri.keyword}"/>' >
					<input type='hidden' name='type' value='<c:out value="${cri.type}"/>' >
					<div class="form-group">
						<label>Bno</label> <input class="form-control" name='bno'
							value='<c:out value="${board.bno}"/>' readonly="readonly">
					</div>

 

수정/삭제 처리는 BoardController에서 redirec 방식으로 동작하므로 type과 keyword 조건도 리다이렉트에 포함해야 한다.

 

BoardController 수정

 

	
	@PostMapping("/modify")
	public String modify(BoardVO board, @ModelAttribute("cri") Criteria cri, RedirectAttributes rttr) {
		log.info("modify: " + board);
		if(service.modify(board)) {
			rttr.addFlashAttribute("result", "sucess");
		}
		rttr.addAttribute("pageNum", cri.getPageNum());
		rttr.addAttribute("amount", cri.getAmount());
		rttr.addAttribute("type", cri.getType());
		rttr.addAttribute("keyword", cri.getKeyword());
		return "redirect:/board/list";
	}
	
	@PostMapping("/remove")
	public String remove(@RequestParam("bno") Long bno, @ModelAttribute("cri") Criteria cri, RedirectAttributes rttr) {
		log.info("remove..." + bno);;
		if(service.remove(bno))
		{
			rttr.addFlashAttribute("result", "success");
		}
		rttr.addAttribute("pageNum", cri.getPageNum());
		rttr.addAttribute("amount", cri.getAmount());
		rttr.addAttribute("type", cri.getType());
		rttr.addAttribute("keyword", cri.getKeyword());
		return "redirect:/board/list";
	}
	

리다이렉트는 GET 방식으로 이루어지므로 추가 파라미터를 처리해야 한다.

javascript 코드 수정.

 

<script type="text/javascript">
	$(document).ready(function() {	
		var formObj = $("form");

		$('button').on("click", function(e){
			e.preventDefault();
			var operation = $(this).data("oper");

			console.log(operation);

			if(operation === 'remove') {
				 formObj.attr("action", "/board/remove");
			 } else if (operation === 'list'){
				//move to list
				formObj.attr("action", "/board/list").attr("method","get");
				var pageNumTag = $("input[name='pageNum']").clone();
				var amountTag = $("input[name='amount']").clone();
				var keywordTag = $("input[name='keyword']").clone();
				var typeTag = $("input[name='type']").clone();
				formObj.empty();
				formObj.append(pageNumTag);
				formObj.append(amountTag);
				formObj.append(keywordTag);
				formObj.append(typeTag);
			}
			formObj.submit();
		});
	});
</script>
<%@include file="../includes/footer.jsp"%>

 

UriComponentsBuilder를 이용하는 링크 생성

 

웹페이지에서 매번 파라미터를 유지하는 일이 번거롭고 힘들 때 유용하다. 

org.springframework.web.util.UriComponentsBuilder는 여러 개의 파라미터들을 연결해서 URL의 형태로 만들어주는 기능을 한다.

 

Criteria 클래스 수정

package org.zerock.domain;

import org.springframework.web.util.UriComponentsBuilder;

import lombok.Getter;
import lombok.Setter;
import lombok.ToString;

@Getter
@Setter
@ToString
public class Criteria {
	private int pageNum;
	private int amount;
	
	private String type;
	private String keyword;
	
	public Criteria() {
		this(1, 10);
	}

	
	public Criteria(int pageNum, int amount) {
		this.pageNum = pageNum;
		this.amount = amount;
	}
	
	public String[] getTypeArr() {
		return type == null? new String[] {} : type.split("");
	}
	
	public String getListLink() {
		UriComponentsBuilder builder = UriComponentsBuilder.fromPath("")
				.queryParam("pageNum", this.pageNum)
				.queryParam("amount", this.getAmount())
				.queryParam("type", this.getType())
				.queryParam("keyword", this.getKeyword());
		return builder.toUriString();
	}
	
}

 

UriComponentBuilder는 queryParam 메서드로 필요 파라미터를 손쉽게 추가할 수 있다.

 

getListLink로 BoardController의 modify와 remove를 다음처럼 간단하게 정리할 수 있다.

 

	@PostMapping("/modify")
	public String modify(BoardVO board, @ModelAttribute("cri") Criteria cri, RedirectAttributes rttr) {
		log.info("modify: " + board);
		if(service.modify(board)) {
			rttr.addFlashAttribute("result", "sucess");
		}
		return "redirect:/board/list" + cri.getListLink();
	}
	
	@PostMapping("/remove")
	public String remove(@RequestParam("bno") Long bno, @ModelAttribute("cri") Criteria cri, RedirectAttributes rttr) {
		log.info("remove..." + bno);;
		if(service.remove(bno))
		{
			rttr.addFlashAttribute("result", "success");
		}
		return "redirect:/board/list" + cri.getListLink();
	}