본문 바로가기
개발입문/SPRING 게시판 만들기

[SPRING] 검색 기능 구현

by 양히◡̈ 2022. 10. 13.

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

검색할 때 검색조건은 총 세 가지로 할 것이다. 내용, 제목, 작성자.

 

이 세 가지 조건 중 단일조건만 사용해서 검색하거나, 두 가지 조건을 섞거나, 세 가지 조건을 섞어서 검색할 수도 있다.

어떻게 조합하여 검색하느냐에 따라 SQL의 쿼리문이 바뀔 것이다.

따라서, 쿼리문을 작성할 때 동적쿼리를 사용해야 한다.

SQL 동적쿼리란 주어진 상황에 따라 쿼리문을 변경하는 것을 말한다.

Reference ▶ https://mybatis.org/mybatis-3/ko/dynamic-sql.html

동적쿼리문은 아래와 같이 만들어 볼 것이다.

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

 

이 동적 쿼리의 처리는 when > 안쪽 trim > foreach를 끝내고 바깥쪽 trim 순서로 되며,

<when>이 모두 동작한 후, 안쪽 <trim>이 실행되면 앞에 각각 or가 붙어 아래와같은 쿼리문이 완성된다.

or title like '%keyword%'
or content like '%keyword%'
or writer like '%keyword%'

그리고 바깥쪽 <trim>까지 실행되면 맨 앞에 or는 지우고 ( 가 붙으며,

맨 뒤에 ) and 가 붙는다.

따라서 최종 쿼리문은 아래와 같이 되는 것이다.

( title like '%keyword%'
or content like '%keyword%'
or writer like '%keyword%'
) and

 

 

 

검색 조건 페이징 처리

검색을 한 후 2페이지가 나왔다면, 2페이지를 눌렀을 때도 검색 상태가 유지되어야 한다.

이런 부분을 구현하기 위해 좀 더 추가해볼 것이다.

Criteria.java 파일을 수정한다.

String변수인 type과 keyword를 추가하고, getTypeArr() 메소드를 추가했다.

public class Criteria {
	private int pageNum; //현재 페이지 번호
	private int amount;	//페이지당 게시물 수
	private String type; //검색타입. 내용(c)+제목(t)+작성자(w)
	private String keyword; //검색어
	
	
	public Criteria() {
		this(1,10); 	//아래쪽 전달값 2개 생성자 호출
	}
	
	public Criteria(int pageNum, int amount) {
		this.pageNum = pageNum;
		this.amount = amount;
	}
	
	public String[] getTypeArr() {
		//검색타입 배열 가져오기
		return type == null ? new String[] {} : type.split("");
		//검색타입이 null 이라면 비어있는 문자열을 배열로 만들고,
		//그렇지 않다면 검색 타입을 한 글자씩 잘라서 문자열 배열로 만듦
	}
}

 

getTypeArr() 라는 메소드를 통해서 Mybatis의 동적 태그를 활용할 수 있다.

이제 BoardMapper.xml 파일을 열어 "getListWithPaging" 조건 부분을 수정할 것이다.

빨간색 네모 부분에 사이에 다음 코드를 추가한다. (코드를 나누면서 CDATA부분도 양쪽에 각각 사용해주어야 한다.)

<select id="getListWithPaging"
    resultType="kr.icia.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>

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

Mybatis는 원하는 속성을 찾을 때 getTypeArr()와 같이 이름에 기반을 두어서 검색하기 때문에,

Criteria에서 만들어준 getTypeArr() 결과인 문자열의 배열이 <foreach>의 대상이 된다.

<choose> 안쪽의 동적 sql은 'OR title ... OR contetnt ... OR writer ...'와 같은 구문을 만들어내게 된다.

따라서 바깥쪽에서는 <trim>을 이용해 맨 앞에 생성되는 OR을 없애주는 것이다.

 

동적 SQL이 제대로 동작하는지

BoardControllerTests에서 살펴보자.

@Test
public void testList3() throws Exception {
    log.info(mockMvc.perform(MockMvcRequestBuilders.get("/board/list")
    .param("pageNum", "2").param("amount", "10")
    .param("type", "TW").param("keyword", "테스트"))
    .andReturn().getModelAndView().getModelMap());
}

 

테스트를 구동하면 아래와 같이 조건에 따라 검색되고 있다.

 

 

<sql><include>를 활용한 SQL문 작성

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

따라서 동적SQL을 처리한 부분을 그대로 복사하여 넣어줄 수 있지만,

수정할 일이 생긴다면 모든 부분을 똑같이 수정하기가 번거로울 수 있다.

Mybatis는 <sql>이라는 태그를 이용해서 SQL의 일부를 별도로 보관하고, 필요한 경우에 include시키는 형태로 사용할 수 있다.

이 방법으로 바꿔볼 것이다.

BoardMapper.xml 에서 <mapper>태그 내에서 가장 상단에 다음 코드를 입력한다.

<sql id="criteria"></sql>

그리고 방금 구현한 <trim> 부분을 모두 잘라낸 후 <sql>태그 안에 붙여넣는다.

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

 

원래 <trim>코드가 있던 자리에는 이렇게 입력한다.

<include refid="criteria"></include>

그리고 맨 아래 <select>태그인 getTotalCount 도 아래와 같이 수정한다.

	<select id="getTotalCount" resultType="int">
		<![CDATA[
		select count(bno) from tbl_board where
		]]> 
		<include refid="criteria"/>
		<![CDATA[
		bno > 0
		]]>
	</select>
</mapper>

 

 

 

 

화면에서 검색 조건 처리

이제 웹 페이지에서 검색이 처리될 수 있도록 구현해 볼 것이다.

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

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

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

- 한글의 경우 GET방식으로 이동하는 경우 문제가 생길 수 있다.

목록화면(list.jsp)에서는 검색조건과 키워드가 들어갈 수 있게 HTML을 수정해야 한다.

list.jsp를 연다.

<table>태그가 끝나는 시점(페이지번호 위)부터 작성한다.

 

<!-- 게시물 목록 끝 -->
<br/>

<!-- 검색창 -->
<div>
    <div class="col-lg-12">
        <form id="searchForm" action="/board/list" method="get">
            &nbsp;&nbsp;&nbsp;<select name="type">
                <option value="" ${pageMaker.cri.type==null?"selected":"" }>--</option>
                <option value="T" ${pageMaker.cri.type eq "T"?"selected":"" }>제목</option>
                <option value="C" ${pageMaker.cri.type eq "C"?"selected":"" }>내용</option>
                <option value="W" ${pageMaker.cri.type eq "W"?"selected":"" }>작성자</option>
                <option value="TC" ${pageMaker.cri.type eq "TC"?"selected":"" }>제목+내용</option>
                <option value="TW" ${pageMaker.cri.type eq "TW"?"selected":"" }>제목+작성자</option>
                <option value="WC" ${pageMaker.cri.type eq "WC"?"selected":"" }>내용+작성자</option>
                <option value="TWC" ${pageMaker.cri.type eq "TWC"?"selected":"" }>제목+내용+작성자</option>
            </select> 
                <input type="text" name="keyword" value="${pageMaker.cri.keyword }" />
                <input type="hidden" name="amount" value="${pageMaker.cri.amount }" />
            <button class="btn btn-warning">Search</button>
        </form>
    </div>
</div>
<br/>

<!-- 페이지번호 시작 -->

 

검색 후에는 <select> 태그나 <input> 태그를 이용해 주소창에 검색 조건과 키워드가 같이 GET방식으로 처리되도록 하였다.

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

웹페이지를 새로고침해보면, 표의 아래쪽에 다음과같은 검색창이 나타나있다.

 

 

검색조건은 제목으로 설정하고, "모달"이라고 입력해보았다.

검색 기능이 잘 나타나며, 검색 조건과 키워드도 유지되고 있다.

 

 

 

 

검색 버튼 유효성 검사

검색버튼을 눌렀을 때 검색조건이나 키워드가 없을 수도 있다.

이럴 경우엔 검색이 되지 않게 해야 한다.

 

list.jsp 의 <script> 부분 $(document).ready(function() { 내에 추가한다.

//검색시 유효성 검사
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();
});

 

브라우저에서 검색 버튼을 클릭하면 <form>태그의 전송은 막고, 페이지의 번호는 1이 되도록 처리한다.

또한, 화면에서 키워드가 없다면 검색을 하지 않도록 제어한다.

검색 종류를 선택하지 않거나, 키워드를 누락했을 경우 안내메세지가 나온다.

 

 

 

 

 

 

페이지 이동 시에도 검색이 유지되게 만들기

검색 후 페이지를 이동했을 때 검색이 풀리는 부분을 보완할 것이다.

페이지를 이동할 때에도 검색 조건과 키워드가 같이 전달되게끔 만들면 된다.

list.jsp의 <form>태그 안에 검색조건과 키워드 값을 물고 갈 수 있도록 input 태그를 추가한다.

 

또한, 검색한 게시글을 읽고 다시 목록으로 돌아왔을 때도 마찬가지로 검색이 풀리기 때문에

get.jspmodify.jsp도 동일하게 추가해야 한다.

<input type="hidden" name="type" value="${pageMaker.cri.type }">
<input type="hidden" name="keyword" value="${pageMaker.cri.keyword }">

이제 검색한 후 페이지를 이동해도 아래와 같이 URL이 잘 전달되고 있다.

 

 

수정/삭제 처리는 BoardController에서 redirect 방식으로 동작하기 때문에, type과 keyword 조건을 같이 redirect 시에 포함시켜야 한다.

BoardController 에서 "/modify"와 "/remove"부분을 찾아 내용을 추가해준다.

// post요청으로 /modify가 온다면 아래 메소드를 수행
@PostMapping("/modify")
public String modify(BoardVO board, Criteria cri, RedirectAttributes rttr) {
    log.info("modify:" + board);
    if (service.modify(board)) {
        rttr.addFlashAttribute("result", "success");
    } // 수정이 성공하면 success 메세지가 포함되어 이동하고, 실패해도 메세지 빼고 이동함
    //addFlashAttribute: 1회성.URL표시창에 전달X
    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, 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";
}

 

redirect는 GET방식으로 이루어지기 때문에 추가적인 파라미터를 처리해야 한다.

modify.jsp에서 다시 목록으로 이동하는 경우 필요한 파라미터만 전송하기 위해서 <form>태그의 모든 내용을 지우고 다시 추가하는 방식을 이용했으므로, keyword와 type도 추가되도록 수정해야 수정창에 있는 목록 버튼을 눌렀을 때도 오류 없이 정상적으로 적용된다.

따라서 modify.jsp <script>부분도 수정해줄 것이다.

아래 사진에 커서가 있는 부분을 수정한다.

} else if (operation === 'list') {
    formObj.attr("action", "/board/list").attr("method","get");

    var pageNumTag=$("input[name='pageNum']");
    var amountTag=$("input[name='amount']");
    var keywordTag=$("input[name='keyword']");
    var typeTag=$("input[name='type']");

    formObj.empty(); //form의 내용 비우기
    formObj.append(pageNumTag);
    formObj.append(amountTag);
    formObj.append(keywordTag);
    formObj.append(typeTag);
}

 

이제 수정창에서도 동작이 잘 된다.

 

 

 

페이징처리와 검색 기능을 get방식에서 메소드 처리로 변경

페이징 처리를 할 때 page,amount,type,keyword를 주소창에 get방식으로 붙여서 보냈는데,

일일이 값을 호출해서 처리하는 것이 아니라 getListLink() 메소드를 만들어서 한꺼번에 처리하도록 변경해볼 것이다.

 

Criteria.java 를 열고 메소드를 추가한다.

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

이렇게 하면 일일이 파라미터로 보내지 않아도 메소드에서 필요한 정보를 넘겨준다.

컨트롤러에서 각 메소드의 return 값에서 이 메소드를 호출하면 된다.

{
	...
	return "redirect:/board/list"+cri.getListLink();
}

 

댓글