이전 게시글은 여기 >>
Servlet 과 JSP를 이용한(모델2 형식) 블로그 만들기(17) - 쿠키를 이용한 새로고침 시 조회수 무한 증가 방어
네X버 블로그도 그렇고, 티스토리도 검색 기능을 제공한다
그러니까 우리 블로그도 검색 기능을 만들어보자.
검색기능을 도와줄 폼은 부트스트랩에서 찾을 것이다.
https://www.w3schools.com/bootstrap4/bootstrap_navbar.asp
부트 스트랩 중 navbar에서 검색 기능이 있는 것을 찾는다.
다.. 다 가져올 필요는 없고 이것만 가져오면 된다.
이제 본격적으로 만들어보자.
먼저 키워드에 검색이 되면 넘어갈 수 있는 액션부터 구현하자.
- home.jsp 의 검색창 코드
<div class="col-md-12 m-2">
<form class="form-inline justify-content-end"
action="/blog/board">
<input type="hidden" name="cmd" value="search"/>
<input type="hidden" name="page" value="0"/>
<input class="form-control mr-sm-2" name="keyword" type="text" placeholder="Search">
<button class="btn btn-success" type="submit">Search</button>
</form>
</div>
이 때 좀 주의할 점이 있는데, action에 암만 /blog/board/cmd=search&page=0 이렇게 걸어도 막상 누르면 쿼리 스트링은 죄다 사라지고 keyword만 쿼리스트링으로 남아버리기 때문에, hidden 속성을 이용해 함께 날려주자.
이제 날아간 요청을 처리해야겠지
지금까지 했다면 순서를 잘 알고 있을 것이다. 라우터 구현- 액션 구현 - (필요하면 DB구현)
- BoardController.java 의 라우터 부분
public Action router(String cmd) {
if(cmd.equals("home")) {
// 홈페이지로 이동
return new BoardHomeAction(); //Board의 목록
}else if(cmd.equals("write")) {
// 글쓰기 페이지로 이동
return new BoardWriteAction();
}else if(cmd.equals("writeProc")) {
// 글쓰기 정보 넘기기
return new BoardWriteProcAction();
}else if(cmd.equals("detail")) {
//상세보기
return new BoardDetailAction();
}else if(cmd.equals("update")) {
//수정페이지
return new BoardUpdateAction();
}else if(cmd.equals("search")) {
//검색
return new BoardSearchAction();
}else if(cmd.equals("updateProc")) {
//수정로직
return new BoardUpdateProcAction();
}else if(cmd.equals("delete")) {
//삭제로직
return new BoardDeleteProcAction();
}
return null;
}
다음, 액션 구현
이 액션은 홈 액션과 별 차이는 없지만.. 이제 검색 키워드가 있다는 것이 차이이다. 그렇기 때문에 홈 액션과 차이는 두지 않으면서 이전 DB repository에 매개변수만 차이를 두며 오버로딩을 한다.
- BoardSearchAction.java
package com.cos.blog.action.board;
import java.io.IOException;
import java.util.List;
import javax.servlet.RequestDispatcher;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import com.cos.blog.action.Action;
import com.cos.blog.model.Board;
import com.cos.blog.repository.BoardRepository;
import com.cos.blog.util.HtmlParser;
import com.cos.blog.util.Script;
public class BoardSearchAction implements Action{
@Override
public void execute(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
// 1. DB연결해서 Board 목록 다 불러와서
int page=Integer.parseInt(request.getParameter("page"));
String keyword=request.getParameter("keyword");
if(request.getParameter("keyword")==null ||
request.getParameter("keyword")=="") {
Script.back("검색 키워드가 없습니다.", response);
return;
}
BoardRepository boardRepository = BoardRepository.getInstance();
// 2. 검색해서 서치로 가져오기.
List<Board> boards = boardRepository.findAll(page,keyword);
int totalCount=boardRepository.countAll(keyword);
// 본문 짧게 가공하기
for (Board board : boards) {
String preview = HtmlParser.getContentPreview(board.getContent());
board.setContent(preview);
}
request.setAttribute("boards", boards);
request.setAttribute("totalCount", totalCount);
RequestDispatcher dis =
request.getRequestDispatcher("home.jsp");
dis.forward(request, response);
}
}
공백은 검색되지 않게 먼저 유효성 검사는 해야겠다.
homeAction에서는 페이징을 위한 페이지만 매개변수로 받았는데(count는 변수가 필요없었지. 다 가져오면 되니까!), 이번엔 필요한 두 가지에 keyword 변수를 추가해서 가져온다.
- BoardRepository.java의 오버로딩 된 매서드들.
public List<Board> findAll(int page,String keyword) {
StringBuilder sb=new StringBuilder();
sb.append("select /*+ INDEX_DESC(BOARD SYS_C008232)*/id, userid, title, content, readcount, createdate ");
sb.append("FROM board ");
sb.append("where title like ? or content like ? "); //여기선 %안먹음.
sb.append("OFFSET ? ROWS FETCH NEXT 3 ROWS ONLY");
final String SQL=sb.toString();
List<Board> boards=new ArrayList<>();
try {
conn=DBConn.getConnection();
pstmt = conn.prepareStatement(SQL);
//물음표 완성하기
pstmt.setString(1, "%"+keyword+"%");
pstmt.setString(2, "%"+keyword+"%");
pstmt.setInt(3, page*3);
rs=pstmt.executeQuery();
//while
while(rs.next()) {
Board board=new Board(
rs.getInt("id"),
rs.getInt("userId"),
rs.getString("title"),
rs.getString("content"),
rs.getInt("readCount"),
rs.getTimestamp("createDate")
);
boards.add(board);
}
return boards;
} catch (Exception e) {
e.printStackTrace();
System.out.println(TAG+"findAll(page,keyword) : "+e.getMessage());
}finally {
DBConn.close(conn, pstmt);
}
return null;
}
public int countAll(String keyword) {
final String SQL="select count(*) from board where title like ? or content like ?";
int totalCount=0;
try {
conn=DBConn.getConnection();
pstmt = conn.prepareStatement(SQL);
//물음표 완성하기
pstmt.setString(1, "%"+keyword+"%");
pstmt.setString(2, "%"+keyword+"%");
rs=pstmt.executeQuery();
//while
if(rs.next()) {
totalCount=rs.getInt("count(*)");
}
return totalCount;
} catch (Exception e) {
e.printStackTrace();
System.out.println(TAG+"count(keyword) : "+e.getMessage());
}finally {
DBConn.close(conn, pstmt);
}
return -1;
}
필자는 쿼리를 바로 변수를 넣는게 아니라 쿼리에 ?를 넣고, pstmt를 사용해서 set자료구조 함수를 이용해서 넣는데, 주의할 점이 있다.
「모델 2 형식은 JSP와 서블릿 모두 사용합니다.」 라는 글을 검색하는데, 이 제목을 모두 넣어야 할까? 아니면 "모델", "JSP", "서블릿" 이라는 단어만 들어가도 검색 되는게 좋을까? 다른 사람들은 모르겠지만 필자는.. 후자가 편하다. 그러니까, 검색될 키워드 변수에 %변수%를 이용해 변수 앞뒤로 무슨 글자가 들어가든 검색 되도록 조치할 예정이다.
이 때, pstmt를 사용한다고 해서 %?% 로 넣으면 인식 못하니까 pstmt.setString(x, %+받아오는 매개변수+%) 로 검색되도록 해야한다.
당연히 % %를 사용하니까 데이터베이스에선 like로 검색해야겠다.
또, 필자는 딱히 검색페이지를 만들지 않고 home.jsp에서 검색 페이지와 홈페이지를 모두 구현할 예정인데, 이것을 위해서 jstl의 c:set함수를 이용하자.
- home. jsp
<%@page import="com.cos.blog.model.Users"%>
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<%@ include file="include/nav.jsp" %>
<c:choose>
<c:when test="${empty param.keyword }">
<c:set var="pageNext" value="/blog/board?cmd=home&page=${param.page+1 }"/>
</c:when>
<c:otherwise>
<c:set var="pageNext" value="/blog/board?cmd=search&page=${param.page+1 }&keyword=${param.keyword }"/>
</c:otherwise>
</c:choose>
<c:choose>
<c:when test="${empty param.keyword }">
<c:set var="pagePrev" value="/blog/board?cmd=home&page=${param.page-1 }"/>
</c:when>
<c:otherwise>
<c:set var="pagePrev" value="/blog/board?cmd=search&page=${param.page-1 }&keyword=${param.keyword }"/>
</c:otherwise>
</c:choose>
<div class="container">
<div class="col-md-12 m-2">
<form class="form-inline justify-content-end"
action="/blog/board">
<input type="hidden" name="cmd" value="search"/>
<input type="hidden" name="page" value="0"/>
<input class="form-control mr-sm-2" name="keyword" type="text" placeholder="Search">
<button class="btn btn-success" type="submit">Search</button>
</form>
</div>
<c:forEach var="board" items="${boards}">
<div class="card m-2" style="width:100%">
<div class="card-body">
<h4 class="card-title">${board.title}</h4>
<p class="card-text">${board.content}</p>
<a href="/blog/board?cmd=detail&id=${board.id }" class="btn btn-primary">상세보기</a>
</div>
</div>
</c:forEach>
<br/>
<ul class="pagination justify-content-center">
<c:choose>
<c:when test="${param.page==0 }">
<li class="page-item disabled"><a class="page-link" href="${pageScope.pagePrev }">Previous</a></li>
</c:when>
<c:otherwise>
<li class="page-item"><a class="page-link" href="${pageScope.pagePrev }">Previous</a></li>
</c:otherwise>
</c:choose>
<c:choose>
<c:when test="${param.page<=totalCount/3-1 }">
<li class="page-item"><a class="page-link" href="${pageScope.pageNext }">Next</a></li>
</c:when>
<c:otherwise>
<li class="page-item disabled"><a class="page-link" href="${pageScope.pageNext }">Next</a></li>
</c:otherwise>
</c:choose>
바로, 받아오는 파라메터 값에 keyword가 있을때와 없을 때의 uri를 다르게 지정하는 방법이다. 검색되는 블로그 페이지 글만 조정하면 되는 문제니까..!
키워드 파라메터가 없으면 cmd=home을 가져오고, 키워드 파라메터가 있으면 cmd=search가 실행되는 형식이다. 이걸 위해서 c:set을 이용해 c:when 내부에 다시 분기할 필요가 없도록 만든다.
결과물