[B -2-24] 검색 처리 1

2019. 10. 3. 23:37Project B (SPMS)/Project B 파트4

반응형

검색 기능과 SQL

 

단일 항목 검색

  • 제목
  • 내용
  • 작성자

 

다중 항목 검색

  • 제목 or 내용
  • 제목 or 작성자
  • 내용 or 작성자
  • 제목 or 내용 or 작성자

오라클은 페이징 처리에 인라인 뷰를 이용하기 때문에 실제로 검색조건에 대한 처리는 인라인 뷰의 내부에서 이뤄져야 한다.

 

단일 항목 검색의 검색 조건에 따라 컬럼이 달라지고, LIKE 처리를 통해 키워드를 사용하게 된다.

 

다중 항목 검색의 검색 조건은 2개 이상이기때문에, 제목이나 내용 중에 '테스트' 라는 문자열이 있는 게시글들을 검색하고 싶으면 주의해야할 점이 있다.

 

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

 


마이바티스의 동적 SQL

 

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

하지만, 다행히도 마이바티스는 동적 태그 기능을 통해 SQL을 파라미터들의 조건에 맞게 조정할 수 있는 기능을 제공한다.

 

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

<if>

단일 항목으로 제목, 내용, 작성자 에 대해 검색해야하는 상황이라고 치면,

 

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

검색조건이 'C' 이면, 내용이 키워드인 항목을 검색

검색조건이 'W' 이면, 작성자이 키워드인 항목을 검색

 

<choose>

if와 달리 여러 상황들 중에 하나의 상황에서만 동작한다.

 

<trim>, <where>, <set>

이 3개는 단독으로 쓰이지 않고, <if>, <choose> 와 같은 태그들을 내포하여 SQL들을 연결해주고, 앞 뒤에 필요한 구문들 (AND, OR, WHERE 등)을 추가하거나 생략하는 역할을 한다.

 

SQL을 작성하다 보면 상황에 따라 WHERE, AND, OR 등이 문제가 발생할 수 있다.

 

예를 들면, 

WHERE ROWNUM <= 20 
AND
TITLE = 'TEST'

만약, 제목에 대한 조건이 없다면 AND라는 키워드는 들어갈 필요가 없지만, 

검색 조건이 추가가 되면 AND 가 필요한 상황이 된다.

이러한 상황에서 사용되는게 <trim>, <where>, <set> 태그 이다.

 

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

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

 

즉, 

bno 값이 존재하는 경우 : select * from tbl_board WHERE bno = 33

bno 가 null인 경우 : select * from tbl_board

 

<trim>

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

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

또한, 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>

리스트, 배열, 맵 등을 이용해서 루프를 처리할 수 있다.

주로 IN 조건에서 주로 사용하지만, 경우에 따라 복잡한 WHERE 조건을 만들 때에도 사용할 수 있다.

 

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()">
            	content = #{val}
            </if>
        	<if test="key == 'W'.toString()">
            	content = #{val}
            </if>
        </trim>
    </foreach>
</trim>

foreach를 배열이나 리스트를 이용하는 경우에는 item 속성만을 이용하면 되고, 

Map 형태로 key, value이용해야 할때는 index와 item 속성을 둘 다 이용한다.

 


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

 

src/main/java

com.spms.domain

Criteria.java

 

...더보기
package com.spms.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 클래스는 typekeyword라는 변수를 추가한다.

getter/setter 롬복을 통해 생성하고,

getTypeArr검색 조건이 각 문자(T, W, C)로 구성되어 있으므로 검색 조건을 배열 만들어서 한번에 처리하기 위함이다.

getTypeArr()를 이용해서 마이바티스의 동적 태그를 활용할 수 있다.

 


src/main/resources

com

spms

mapper

BoardMapper.xml

getListWithPaging()

...더보기
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
  PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
  "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.spms.mapper.BoardMapper">

	<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.spms.domain.BoardVO">
	<![CDATA[
		select 
			BNO, TITLE, "CONTENT", WRITER, REGDATE, UPDATEDATE
		from 
		(
			select /*+INDEX_DESC(TBL_BOARD PK_BOARD) */
				rownum RNUM, BNO, TITLE, "CONTENT", WRITER, REGDATE, UPDATEDATE 
			from 
				TBL_BOARD
      		where 
	]]>
		<include refid="criteria"></include>
	<![CDATA[
			rownum <= #{pageNum} * #{amount}
		)
		where RNUM > (#{pageNum} -1) * #{amount}   
	]]>
	</select>
	
	<!-- 전체 레코드 구하기 -->
	<select id="getTotalCount" resultType="int">
		select count(*)
		from TBL_BOARD
		where 
		<include refid="criteria"></include>
		bno > 0
	</select>
	
	<!-- 등록 (선택키 활용) -->
	<insert id="insertSelectKey">
		<selectKey keyProperty="bno" order="BEFORE" resultType="long">
			select SEQ_BOARD.nextval 
			from DUAL
		</selectKey>
		insert into TBL_BOARD (
			BNO, TITLE, CONTENT, WRITER
		)
		values (
			#{bno}, #{title}, #{content}, #{writer}
		)
	</insert>
	
	<!-- 조회 -->
	<select id="read" resultType="com.spms.domain.BoardVO">
		select * 
		from TBL_BOARD 
		where BNO = #{bno}
	</select>
	
	<!-- 수정 -->
	<update id="update">
		update TBL_BOARD set 
				TITLE = #{title},
				CONTENT = #{content},
				WRITER = #{writer},
				UPDATEDATE = SYSDATE
		where BNO = #{bno}
	</update>
	
	<!-- 삭제 -->
	<delete id="delete">
		delete TBL_BOARD
		where BNO = #{bno}
	</delete>
	
</mapper>

 

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

 

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

마이바티스는 원하는 속성을 찾을 때 getTypeArr()와 같이 이름에 기반을 둬서 검색하기때문에 Criteria에서 만들어놓은 getTypeArr() 결과인 문자열 배열이 <foreach> 의 대상이 된다.

(마이바티스는 엄격하게 자바 빈즈의 규칙을 따르지 않고, get/set 메서드만을 활용하는 방식이다)

 

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

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

 


src/test/java

com

spms

mapper

BoardMapperTests.java

 

...더보기
package com.spms.mapper;

import java.util.List;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

import com.spms.domain.BoardVO;
import com.spms.domain.Criteria;

import lombok.Setter;
import lombok.extern.log4j.Log4j;

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = {com.spms.config.RootConfig.class} )
@Log4j
public class BoardMapperTests {

	@Setter(onMethod_ = @Autowired)
	private BoardMapper boardMapper;

	public void testPaging() {
		Criteria cri = new Criteria();

		// 10개씩 3페이지
		cri.setPageNum(3);
		cri.setAmount(10);

		List<BoardVO> list = boardMapper.getListWithPaging(cri);

		list.forEach(board -> log.info(board));
	}

	@Test
	public void testSearch() {

		Criteria cri = new Criteria();
		cri.setKeyword("다이얼로그");
		cri.setType("TC");

		List<BoardVO> list = boardMapper.getListWithPaging(cri);

		list.forEach(board -> log.info(board));
	}
}

 

testSearch()는 Criteria 객체의 typekeyword를 넣어서 원하는 SQL이 생성되는지 확인하기 위함이다.

중요한 것은 실행 결과가 아니라 만들어지는 SQL이다.

 

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

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

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

 

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

 

<sql> 태그는 id라는 속성을 이용해서 필요한 경우에 동일한 SQL의 일부를 재사용할 수 있게 한다.

 


화면에서 검색 조건 처리

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

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

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

 

 

views/board

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">게시판 목록</h1>
	</div>
	<!-- /.col-lg-12 -->
</div>
<!-- /.row -->
<div class="row">
	<div class="col-lg-12">
		<div class="panel panel-default">
			<div class="panel-heading">게시글 목록 페이지<button id='regBtn' type="button" class="btn btn-xs pull-right btn-primary">게시글 등록</button></div>
			<!-- /.panel-heading -->
			<div class="panel-body">
				<table class="table table-striped table-bordered table-hover">
					<tbody>
					<c:forEach items="${list }" var="board">
						<tr>
							<td>
								<font color="Gray">
									<c:out value="${board.bno}"/>&nbsp;/&nbsp;
									<c:out value="${board.writer}"/>&nbsp;/&nbsp;
									<fmt:formatDate value="${board.regdate}" pattern="yyyy-MM-dd"/>&nbsp;/&nbsp;
									<fmt:formatDate value="${board.updateDate}" pattern="yyyy-MM-dd"/>
								</font><br />
								<font size="3px">
									<a class='move' href="<c:out value="${board.bno}"/>">
										<c:out value="${board.title}"/>
									</a>
								</font>
							</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="">--</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' 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>
				</div>
				<!--  페이지네이션 끝 -->
				
			<!-- 이전 현재 다음 버튼의 a 태그동작을 막기위한 폼 태그 -->
			<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}'>
			</form>
			<!-- 폼 태그 종료 -->
			
			<!-- 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">게시글 처리 결과</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>
			<!-- /.panel-body -->
		</div>
		<!-- /.panel -->
	</div>
	<!-- /.col-lg-12 -->
</div>
<!-- /.row -->
</div>
<!-- /#page-wrapper -->
<%@ include file="../includes/footer.jsp"%>

<script type="text/javascript">
	$().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();

						});
						
					});
</script>

검색 조건키워드가 들어갈 수 있게 수정한다.

페이지 처리 바로 위쪽에 추가


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

테스트는 항상 영문과 한글 둘 다 테스트해야 한다.

반응형

'Project B (SPMS) > Project B 파트4' 카테고리의 다른 글

[B -2-26] 검색 처리 3  (0) 2019.10.04
[B -2-25] 검색 처리 2  (0) 2019.10.04