Chapter 15 검색처리 - oneso123456789/2022 GitHub Wiki

feedback

15 검색처리

게시물 관리에서 마지막은 다양한 검색 처리임
검색 기능은 다시 검색 조건과 키워드로 나누어 생각해 볼 수 있음
검색조건은 일반적으로 <select> or <checkbox>를 이용하는 경우가 많음

과거엔 <checkbox>를 이용하는 경우가 많았지만 요즘엔 일반 사용자들은 <select>
관리자용 이나 검색 기능이 강한 경우에만 <checkbox>를 이용하는 형태가 대부분임

예제는 가장 흔한 <select> 태그를 이용해서 검색 기능과 화면을 처리함

15.1 검색 기능과 SQL

게시물의 검색 기능은 두가지로 분류 가능

  • 제목/내용/작성자와 같은 단일 항목 검색
  • 제목 or 내용, 제목 or 작성자, 내용 or 작성자, 제목 or 작성자와 같은 다중 항목 검색

게시물의 검색이 붙으면 가장 신경 쓰이는 부분은 역시 SQL 쪽임
오라클은 페이징 처리에 인라인뷰를 이용하기 때문에 실제로 검색 조건에
대한 처리는 인라인뷰의 내부에서 이루어져야 함

단일 항목의 검색은 검색 조건에 따라서 칼럼이 달라지고,
LIKE 처리를 통해서 키워드를 사용하게 됨

만일 2페이지에 해당하는 데이터를 제목으로 검색하고,
키워드는 test라고 한다면 다음과 같은 SQL문이 작성됨

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

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

15.1.1 다중 항목 검색

문제는 2개 이상의 조건이 붙는 다중 항목 검색임
예를 들어 title에 test나 content에 새로라는 문자열이 있는 게시물을
검색하고 싶다면 다음과 같이 작성될 것이라고 예상됨

select 
* 
from
    (
        select  /*+Index_desc(tbl_board pk_board)  */
            rownum rn , bno, title, content, writer, regdate, updatedate
        from 
            tbl_board
        where
         title like '%test%' or content like '%새로%'
    
        and rownum <= 20
    )
where rn > 10;

해당 SQL문은 구문 자체는 이상이 없지만, 실제 동작시 10개의 데이터가 아니라
많은 양의 데이터가 나오는 것을 볼 수 있음

이렇게 많은 양의 데이터가 나온 이유는 위 SQL문의 AND연산자가 OR연산자보다
우선 순위가 높기 때문에 ROWNUM이 20보다 작거나 같으면서(AND)
제목에 test라는 문자열이 있거나(OR) 내용에 새로라는 문자열이 있는경우는
많기 때문에 결과에 많은 양의 데이터를 가져옴

AND와 OR이 섞여있는 SQL문을 작성할 때에는 우선 순위 연산자인 ()를 이용해서 OR 조건들을 처리해야함

아래와 같은 sql문 실행시 10개의 결과만 보여줌

select 
* 
from
    (
        select  /*+Index_desc(tbl_board pk_board)  */
            rownum rn , bno, title, content, writer, regdate, updatedate
        from 
            tbl_board
        where
         (title like '%test%' or content like '%새로%')
        and rownum <= 20
    )
where rn > 10;

2022-05-18 오후 6시548분 15.2 myBatis의 동적 SQL전까지함

15.2 MyBatis의 동적 SQL

SQL문에서 느끼는 점은 검색 조건이 변하면 SQL의 내용 역시 변하기 때문에 XML이나 어노테이션과 같이
고정된 문자열을 작성하는 방식으로는 제대로 처리할 수 없다는 사실임

다행히 MyBatis는 동적(Dynamic) 태그 기능을 통해서 SQL을 파라미터들의 조건에 맞게 조정할 수 있는 기능을 제공함

MyBatis의 동적 태그는 약간의 구문을 이용해서 전달되는 파라미터를 가공해서 경우에 따라 다른 SQL을 만들어서 실행 할 수 있음


15.2.1 MyBatis의 동적 태그들

MyBatis는 기존의 iBatis에서 발전하면서 복잡했던 동적 SQL을 작성하는 태그들이 많이 정리됨
따라서 다음과 같이 몇 가지의 태그들만을 이용함

  • if
  • choose(when,otherwise)
  • trim(where,set)
  • foreach

<if>

if는 test라는 속성과 함께 특정한 조건이 true가 되었을때 포함된 SQL을 사용하고자 할 때 작성함
예를들어 , 단일 항목으로 제목(title), 내용(content), 작성자(writer)에 대하서 검색하는 상황이라고 가정하면

  • 검색 조건이 T면 제목(title)이 키워드(keyword)인 항목을 검색
  • 검색 조건이 C면 내용(content)이 키워드(keyword)인 항목을 검색
  • 검색 조간이 W면 작성자(writer)가 키워드(keyword)인 항목을 검색

위와 같은 경우 MyBatis에서는 XML에서 다음과 같이 작성할 수 있음

<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 안에 들어가는 표현식(experssion)은 OGNL포현식 이라는것을 이용함
하지만 이 방식을 사용시 XSS공격 방식에 취약해서 네이버에서 만든 필터인 Lucy필터를 사용하면 방어하기 수월함
https://github.com/naver/lucy-xss-filter

<choose>

if와 달리 choose는 여러 상황들 중 하나의 상황에서만 동작함
Java 언어의 if~else나 JSTL의 와 유사함

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

<otherwise>는 위의 모든 조건이 충족되지 않을 경우에 사용함


<trim>, <where>, <set>

trim, where, set은 단독으로 사용되지 않고, <if>,<chose>와 같은 태그들을 내포하여 SQL들을 연결해 주고,
앞 뒤 필요한 구문들(AND, OR, WHERE)등을 추가하거나 생략하는 역할을 해줌

SQL을 작성하다 보면 상황에 따라서 WHERE나 AND, OR등이 문제가 되는 상황이 발생할 수도 있음
예를 들어, WHERE ROWNUM <=20은 문제가 없지만 검색 조건이 들어가면 문제가 될 수 있음
p331 그림참고

위의 그림과 같이 만일 검색 조건이 없다면 AND라는 키워드가 들어갈 필요가 없지만,
검색 조건이 추가되면 AND가 필요한 상황이 됨
where, trim, set은 이러한 상황에서 필요한 키워드를 붙이거나 빼는 상황에서 사용함

<where>

<where>의 경우 태그 안쪽에서 SQL이 생성될 때는 WHERE 구문이 붙고, 그렇지 않는 경우에는 생성되지 않음

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

위와 같은 경우는 bno 값이 null이 아니라면 select * from tbl_board WHERE bno = xx라는구문이
bno 값이 null이라면 select * from tbl_board라는 구문이 실행됨

<trim>

<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 속성을 지정가능
bno 값이 존재하는 경우select * from tbl_board WHERE bno = 33 and rownum=1 bno가 null인 경우 select * from tbl_board WHERE rownum = 1

<foreach>

<foreach>는 List, 배열 , 맵 등을 이용해서 루프를 처리할 수 있음
주로 IN 조건에서 많이 사용하지만, 경우에 따라서는 복잡한 WHERE 조건을 만들때에도 사용할 수 있음

예를들어서, 제목(T)은 TTTT로 내용(C)은 CCCC라는 값을 이용한다면 Map형태로 작서이 가능함

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 속성을 둘 다 이용함
전달된 값에 따라서 다음과 같이 처리됨

select * fron tbl_board
    where ( content = ?
      OR title = ? )

INFO: jdbc.sqlonly - select * from tbl_board where ( content = 'CCCC' OR title = 'TTTT' )

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

페이징 처리에 사용했던 Criteria의 의도는 단순히 pageNum과 amount라는 파라미터를 수집하기 위해서임
페이징 처리에 검색 조건 처리가 들어가면 Criteria역시 변화가 필요함

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

확장 방법으로는 상속 방법을 이용하거나 직접 Criteria 클래스를 수정하는 방식을 생각해 볼 수 있음
여기선 직접 Criteria클래스를 수정하겠음

Criteria.Class 수정

package com.crow.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("");
    }

}

Criteria 클래스는 type과 keyword라는 변수를 추가함
getter/setter는 Lombok을 통해서 생성, getTypeArr는 검색 조건이 각 글자(T,W,C)로 구성되어 있으므로
검색 조건을 배열로 만들어서 한 번에 처리하기 위함임
getTypeArr()를 이용해서 MyBatis의 동적 태그를 활용할 수 있음

15.3.1 BoardMapper.xml에서 Criteria 처리

BoardMapper.xml은 기존의 getListWithPaging()을 수정해서 동적 SQL을 처리함

BoardMapper.xml의 검색 및 페이징 처리

<select id="getListWithPaging"
        resultType="com.crow.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>

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

<foreach>를 이용해서 검색 조건들을 처리하는데 typeArr이라는 속성을 이용함
MyBatis는 원하는 속성을 찾을 때 getTypeArr()과 같이 이름에 기반을 두어서 검색함
그러므로 Criteria에서 만들어둔 getTypeArr() 결과인 문자열의 배열이 <foreach>의 대상이됨
(MyBatis는 엄격하게 Java Beans의 규칙을 따르지 않고, get/set 메서드만을 활용하는 방식임)

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

위의 동적 SQL은 상황에 따라서 다음과 같은 SQL을 생성함

Criteria cri = new Criteria();
        cri.setKeyword("곰곰");
        cri.setType("TCW");
INFO : jdbc.sqltiming - 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 
( title like '%'||'곰곰'||'%' OR content like '%'||'곰곰'||'%' OR writer like '%'||'곰곰'||'%' ) 
AND rownum <= 1 * 10 ) where rn > (1 -1) * 10 

동적 SQL은 경우에 따라서 여러 종류의 SQL이 생성될 수 있으므로 제대로 종작하는지 반드시 여러 번의 확인 과정을 거쳐야함
기존에 BoardMapperTests를 만들어 두었으니 이를 이용해서 테스트 코드를 작성하겠음

####src/test/java BoardMapperTests 클래스의 일부

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

testSearch()는 Criteria 객체의 type과 keyword를 넣어서 원하는 SQL이 생성되는지 확인하기 위함임
중요한것은 실행 결과가 아니라 실행할 때 만들어지는 SQL임

내가 작성한 테스트 코드를 기반으로 p337~338의 각 상황에 맞게 SQL이 올바르게 만들어지는지 확인해야함

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

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

이 경우 가장 간단한 방법은 동적 SQL을 처리하는 부분을 그대로 복사해서 넣어줄 수 있지만,
만일 동적 SQL을 수정하는 경우에는 매번 목록을 가져오는 SQL과 데이터 개수를 처리하는 SQL 쪽을 같이 수정해야함

MyBatis는 <sql>이라는 태그를 이용해서 SQL의 일부를 별도로 보관하고, 필요한 경우에 include시키는 형태로 사용할 수 있음
<sql>태그는 id라는 속성을 이용해서 동일한 SQL의 일부를 재사용 할 수 있음

BoardMapper.xml의 목록과 데이터 개수 처리 <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="com.crow.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>



15.4 하면에서 검색 조건 처리

화면에서 검색 조건은 다음과 같은 사항들을 주의해서 개발해야함

  • 페이지 번호가 파라미터로 유지되어던 것처럼 검색 조건과 키워드 역시 항상 화면 이동시 같이 전송되어야 함
  • 화면에서 검색 버튼을 클릭하면 새로 검색을 한다는 의미이므로 1페이지로 이동함
  • 한글의 경우 GET 방식으로 이동하는 경우 문제가 생길 수 있으므로 주의해야 함

15.4.1 목록 화면에서의 검색 처리

목록 화면인 list.jsp에서는 검색 조건과 키워드가 들어 갈 수 있게 HTML을 수정해야함
views 폴더 내의 list.jsp를 수정해서 페이지 처리 바로 위쪽에 폼을 추가해서 파라미터를 넘겨주는 형태임

list.jsp에 검색 조건 처리

</c:forEach>
				</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">제목내용</option>
				            <option value="TW">재목작성자</option>
				            <option value="TWC">제목내용작성자</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>

구문오류 발생해서 서치 버튼이 생성 안됬음 디버깅일지 2022-05-23 참고

페이징 처리를 위해서 만들어둔 <form>태그에 <select><input>태그를 추가함

<form><button>의 기본 동작은 submit이므로 별도의 처리 없이 검색이 되는지 확인
항상 테스트는 영문과 한글 모두 테스트 해봐야함

Chome 브라우저는 한글로 검색하는 경우에 주소창에는 한글이 깨지지 않고 나오지만 실제로는 깨져서 전송됨
따라서 검색이 처리된 후 몇 가지 문제가 있다는걸 알게됨

  1. 3페이지를 보다가 검색하면 3페이지로 이동하는 문제,
  2. 검색후 페이지를 이동하면 검색 조건이 사라지는 문제,
  3. 검색 후 화면에서는 어떤 검색 조건과 키워드를 이용했는지 알 수 없는 문제

검색 버튼의 이벤트 처리

여러 문제들 중에서 검색 버튼을 클릭하면 검색은 1페이지를 하도록 수정하고,
화면에 검색 조건과 키워드가 보이게 하는 작업을 우선 진행

list.jsp의 검색 버튼의 이벤트 처리

						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();
								});

검색 버튼 이벤트 처리중 오류 발생 디버깅일지 참고 2022-05-23

해당 코드는 브라우저에서 검색 버튼을 클릭하면

태그의 전송은 막고,
페이지 번호는 1이 되도록 처리하고 화면에서 키워드가 없다면 검색하지 않도록 제어함

검색 후에는 주소창에 검색 조건과 키워드가 같이 GET방식으로 처리되므로
이를 이용해서 <select>, <input>태그의 내용을 수정해야함

list.jsp에서 검색조건과 키워드 보여주는 부분

						<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':''}" />>제목내용</option>
								<option value="TW"
									<c:out value="${pageMaker.cri.type eq 'TW'?'selected':''}" />>재목작성자</option>
								<option value="TWC"
									<c:out value="${pageMaker.cri.type eq 'TCW'?'selected':''}" />>제목내용작성자</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>

<select> 태그의 내부는 삼항 연산자를 이용해서 새당 조건으로 검색되었다면
selected라는 문자열을 출력하게 해서 화면에서 선택된 항목으로 보이도록 해줌

다음으로 페이지 번호를 클리갷서 이동할 때에도 검색 조건과 키워드는 같이 전달되어야 하므로
페이지 이동에 사용한 <form> 태그를 아래와 같이 수정해줌

list.jsp의 일부

			<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='${pageMaker.cri.type}'>
				<input type='hidden' name='keyword' value='${pageMaker.cri.keyword}'>
			</form>

검색 조건과 키워드에 대한 처리가 되면 검색 후 페이지를 이동해서 동일한 검색 사항들이
계속 유지되는 것을 볼 수 있음

2022-05-23 오후 6시 56분 15.4.2 조회 페이지에서 검색 처리 전까지함

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

목록 페이지에서 조회 페이지로 이동은 이미 <form>태그를 이용해서 처리했음
다만 조회 페이지는 아직 Criteria의 type과 keyword에 대한 처리가 없기 때문에
이 부분을 수정해야함

views/board/get.jsp의 일부

				<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>



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

조회 페이지에서 수정/삭제 페이지로의 이동은 GET 방식을 통해서 이동,
이동 방식 역시 <form> 태그를 이용하는 방식이므로 기존의 <form> 태그에 추가적인 type 과 keyword 조건만을 추가함

views/board/modify.jsp의 일부

				<form role="form" action="/board/modify" method="post">

					<!-- paging  -->
					<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}"/>'>

(코드는 sts의 자동 줄마춤시 이렇게 바뀜 몇번 고쳐봤지만 자동 줄맞춤 기능을 안쓸수 없어서 그냥 놔두기로함)


수정/삭제 처리는 BoardController에서 redirect 방식으로 동작하므로
type과 keyword 조건을 같이 리다이렉트 시에 포함시켜야함

com.crow.controller.BoardController의 일부

    @PostMapping("/modify")
    public String modify(BoardVO board, @ModelAttribute("cri") Criteria cri, RedirectAttributes rttr) {

        log.info("BoardController modify:" + board);

        if (service.modify(board)) {
            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";
    }

    @PostMapping("/remove")
    public String remove(@RequestParam("bno") Long bno, @ModelAttribute("cri") Criteria cri, RedirectAttributes rttr) {

        log.info("BoardController: remove..." + bno);

        if (service.remove(bno)) {
            rttr.addFlashAttribute("reult", "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 방식으로 이루어지기 때문에 추가적인 파라미터를 처리해야함

modify.jsp에서는 다시 목록으로 이동하는 경우에 필요한 파라미터만 전송하기 위해서<form>태그의 모든 내용을
지우속 다시 추가하는 방식을 이용했으므로 keyword와 type 역시 추가하도록 JavaScript코드를 수정해야함

modify.jsp의 일부

<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");
				alert("삭제되었습니다")
			} else if (operation === 'list') {
				// move 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>

수정/조회 화면에서 어떤 작업을 하던지 다시 목록 페이지로 검색조건이 유지되는지 확인해야함


UriComponentsBuilder를 이용하는 링크 생성

웹페이지에서 매번 파라미터를 유지하는 일이 번거롭고 힘들다면 한 번쯤
UriComponentsBuilder라는 클래스를 이용해볼 필요가 있음
org.springframework.web.util.UriComponentsBuilder는 여러 개의 파라미터들을 연결해서
URL의 형태로 만들어주는 기능을 가지고 있음

URL을 만들어주면 리다이렉트를 하거나, <form> 태그를 사용하는 상황을 많이 줄여줄 수 있음
검색 조건을 유지하는 com.crow.domain.Criteria 클래스에 링크를 생성하는 기능을 추가해줌

Criteria 클래스의 일부

        
        UriComponentsBuilder builder = UriComponentsBuilder.fromPath("")
                .queryParam("pageNum", this.pageNum)
                .queryParam("amount", this.getAmount())
                .queryParam("type", this.getType())
                .queryParam("keyword", this.getKeyword());
                
        
        return builder.toUriString();
    }

UriComponentsBuilder는 queryParam()이라는 메서드를 이용해서 필요한 파라미터들을 손쉽게 추가 할 수 있음

예를 들어 아래와 같은 조건들로 Criteria가 생성된다고 가정해보겠음

        Criteria cri = new Criteria();

        cri.setPageNum(3);
        cri.setAmount(20);
        cri.setKeyword("새로");
        cri.setType("TC");

위와 같은 데이터를 가진 Criteria의 getListLink()의 결과는 ?pageNum=3&amount=20&type=TC&keyword=%EC%83%88%EB%A1%9C
와 같이 GET 방식에 접학합 URL 인코딩된 결과로 만들어짐
(가장 편리한 점은 한글 처리에 신경쓰지 않아도 됨)

또한 getListLink()를 이용하면 BoardController의 modify()와 remove()를 다음과 같이 간단히 정리 가능

com.crow.controller.BoardController의 일부

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


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

UriComponentsBuillder로 생성된 URL은 화면에서도 유용하게 사용 될 수 있음
주로 JavaScript를 사용할 수 없는 상황에서 링크를 처리해야 하는 상황에서 사용됨

⚠️ **GitHub.com Fallback** ⚠️