ㅁ 검색 요청시 검색 결과가 화면에 뿌려진다. 그 검색 결과에도 페이징처리를 해본다.
- 검색 결과를 별도로 search.jsp 등의 페이지로 만들지 않고 /board/list.do를 재활용한다.
- 작성자, 제목, 내용으로 검색할 수 있다.
ㅁ list.jsp
< %@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
< c:set var = "contextPath" value = "${pageContext.request.contextPath}" />
<! DOCTYPE html >
< html >
< head >
< meta charset = "UTF-8" >
< title > Insert title here </ title >
< style >
#boardList th , #boardList td:not ( :nth-child ( 2 )){ text-align : center ;}
#boardList > tbody > tr:hover { cursor : pointer ;}
</ style >
</ head >
< body >
< div class = "container p-3" >
<!-- Header, Nav start -->
< jsp:include page = "/WEB-INF/views/common/header.jsp" />
<!-- Header, Nav end -->
<!-- Section start -->
< section class = "row m-3" style = " min-height: 500px" >
< div class = "container border p-5 m-4 rounded" >
< h2 class = "m-4" > 게시글 목록 </ h2 >
< br >
<!-- 로그인후 상태일 경우만 보여지는 글쓰기 버튼-->
< a class = "btn btn-secondary" style = " float:right" href = "" > 글쓰기 </ a >
< br >
< br >
< table id = "boardList" class = "table table-hover" align = "center" >
< thead >
< tr >
< th width = "100px" > 번호 </ th >
< th width = "400px" > 제목 </ th >
< th width = "120px" > 작성자 </ th >
< th > 조회수 </ th >
< th > 작성일 </ th >
< th > 첨부파일 </ th >
</ tr >
</ thead >
< tbody >
< c:choose >
< c:when test = "${ empty list }" >
< tr >
< td colspan = "6" > 조회된 게시글이 없습니다. </ td >
</ tr >
</ c:when >
< c:otherwise >
< c:forEach var = "b" items = "${ list }" >
< tr >
< td > ${ b.boardNo } </ td >
< td > ${ b.boardTitle } </ td >
< td > ${ b.boardWriter } </ td >
< td > ${ b.count } </ td >
< td > ${ b.registDt } </ td >
< td > ${ b.attachCount > 0 ? '*' : '' } </ td >
</ tr >
</ c:forEach >
</ c:otherwise >
</ c:choose >
</ tbody >
</ table >
< br >
< ul id = "paging_area" class = "pagination d-flex justify-content-center" >
< li class = "page-item ${ pi.currentPage == 1 ? 'disabled' : '' }" >
< a class = "page-link" href = "${ contextPath }/board/list.do?page=${pi.currentPage-1}" > Previous </ a >
</ li >
< c:forEach var = "p" begin = "${ pi.startPage }" end = "${ pi.endPage }" >
< li class = "page-item ${ pi.currentPage == p ? 'active' : '' }" >
< a class = "page-link" href = "${contextPath}/board/list.do?page=${ p }" > ${ p } </ a >
</ li >
</ c:forEach >
< li class = "page-item ${ pi.currentPage == pi.maxPage ? 'disabled' : '' }" >
< a class = "page-link" href = "${ contextPath }/board/list.do?page=${pi.currentPage+1}" > Next </ a >
</ li >
</ ul >
< br clear = "both" >< br >
< form id = "search_form" action = "${contextPath }/board/search.do" method = "get" class = "d-flex justify-content-center" >
< input type = "hidden" name = "page" value = "1" >
< div class = "select" >
< select class = "custom-select" name = "condition" >
< option value = "user_id" > 작성자 </ option >
< option value = "board_title" > 제목 </ option >
< option value = "board_content" > 내용 </ option >
</ select >
</ div >
< div class = "text" >
< input type = "text" class = "form-control" name = "keyword" value = "${search.keyword}" > <!-- 검색후 search(map객체)에서 가져옴. el구문 특성상 search가 없어도 오류 안나고 빈값으로 보임. -->
</ div >
< button type = "submit" class = "search_btn btn btn-secondary" > 검색 </ button >
</ form >
< c:if test = "${ not empty search }" >
< script >
$ ( document ). ready ( function (){
$ ( "#search_form select" ). val ( '${search.condition}' );
// 검색 후의 페이징바 클릭시 search_form을 강제로 submit(단, 페이지 번호는 현재 클릭한 페이지 번호로 바꿔서)
$ ( "#paging_area a" ). on ( "click" , function (){
let page = $ ( this ). text (); // Previous | Next | 페이지번호
if ( page == 'Previous' ){
page = $ { pi . currentPage - 1 };
} else if ( page == 'Next' ){
page = $ { pi . currentPage + 1 };
}
$ ( "#search_form input[name=page]" ). val ( page );
$ ( "#search_form" ). submit ();
return false ; // 기본이벤트(href='/board/list.do' url 요청)가 동작 안되도록 막는다. 이벤트가지고 pervent 어쩌구를 호출해서 막을 수도 있지만 이벤트 핸들러로
})
})
</ script >
</ c:if >
</ div >
</ section >
<!-- Section end -->
<!-- Footer start -->
< jsp:include page = "/WEB-INF/views/common/footer.jsp" />
<!-- Footer end -->
</ div >
</ body >
</ html >
- key값 요청시 전달값 condition과 keyword를 줬다.
- 작성자, 제목, 내용의 key값(value 속성값)을 나중에 비교할 비교대상 컬럼명으로 줬다.
- form 요소 선택을 위해 id를 줬다.
- form 요소 action을 작성했다.
- 검색은 get방식이다.
- 검색 결과 중에 몇번 페이지를 요청할 건지 페이지 번호도 넘겨줘야 한다.
검색 submit을 했을 때는 당연히 1번 페이지 요청이다.
- 페이지 번호도 넘기기 위해 hidden 타입 input 요소를 뒀다.
ㅁ BoardController
package com . br . spring . controller ;
import java . util . List ;
import java . util . Map ;
import org . springframework . stereotype . Controller ;
import org . springframework . ui . Model ;
import org . springframework . web . bind . annotation . GetMapping ;
import org . springframework . web . bind . annotation . RequestMapping ;
import org . springframework . web . bind . annotation . RequestParam ;
import com . br . spring . dto . BoardDto ;
import com . br . spring . dto . PageInfoDto ;
import com . br . spring . service . BoardService ;
import com . br . spring . util . PagingUtil ;
import lombok . RequiredArgsConstructor ;
@ RequestMapping ( "/board" )
@ RequiredArgsConstructor
@ Controller
public class BoardController {
private final BoardService boardService ;
private final PagingUtil pagingUtil ;
// 메뉴 바에 있는 메뉴 클릭시 /board/list.do => 1번 페이지 요청
// 페이징 바에 있는 페이지 클릭시 /board/list.do?page=xx
@ GetMapping ( "/list.do" )
public void list (@ RequestParam ( value = "page" , defaultValue = "1" ) int currentPage , Model model ) {
int listCount = boardService . selectBoardListCount ();
PageInfoDto pi = pagingUtil . getPageInfoDto ( listCount , currentPage , 5 , 5 );
List < BoardDto > list = boardService . selectBoardList ( pi );
model . addAttribute ( "pi" , pi );
model . addAttribute ( "list" , list );
// return "board/list"; 생략해도 됨.
}
@ GetMapping ( "/search.do" )
public String search (@ RequestParam ( value = "page" ) int currentPage
, @ RequestParam Map < String , String > search
, Model model ) {
// 요청시 전달값을 매개변수를 둬서 받기
// 지금은 무조건 page라는 key값으로 1번이 올거라 defaultValue를 안썼다. 써도 된다.
// 요청하는 페이지 번호는 currentPage에 담긴다.
// 지금 condition과 keyword 값을 받아줄 dto 커맨드 객체가 없다. String 형 변수 2개를 둬서 받아도 된다. 근데 어차피 넘길때는 Map에 담아서 넘기도록 서비스 impl를 설계했었다.
// ajax때 했던 @RequestBody로 Map으로 바로 받을 수 있었다.
// 아니면 @RequestParam Map<String, String> search로 해도 된다. 이렇게 해본다. 알아서 key과 value값이 담긴다.
// Map<String, String> search => {condition=user_id|board_Title|board_content, keyword=란}
int listCount = boardService . selectSearchListCount ( search );
PageInfoDto pi = pagingUtil . getPageInfoDto ( listCount , currentPage , 5 , 5 );
List < BoardDto > list = boardService . selectSearchList ( search , pi );
model . addAttribute ( "pi" , pi );
model . addAttribute ( "list" , list );
model . addAttribute ( "search" , search ); // list.jsp가 로드되는 경우가 2가지 경우다. /search.do라는 요청으로 로드될 때는 search라는 key값으로 Map이 담겨있다.
return "board/list" ; // 이미 만든 list.jsp로 포워딩
}
}
- list.jsp에서 pi와 list에서 뽑아내고 있다. Model 객체에 다음과 같은 key값으로 담ㄱ ㅗ포워딩 해줘야 한다.
- 요청시 전달값이 다수가 넘어올경우 보통 커맨득객체로 바인딩을 한다.
만약 이걸 담을만한 커맨드 객체가 없다면 Map같은걸로 받아낼 수 있다.
어차피 Map에 담아내야 되기 때문ㅇ ㅔMap을 세팅했다.
만약 String condition, String keyword 이렇게 각각의 변수로 요청시 전달값을 바인딩할 수 있다.
그리고 나중에 Map에 put으로 담기.
Map<String, String> search = new HashMap<>();
search.put("condition", condition);
- 스프링에서는 기본적으로 @RequestParam이다. 보이지 않을 뿐이지 다 붙어있다고 보면 된다.
- ajax때는 @RequestBody를 썼다. 그때도 비동기식을 ㅗ전달된 데이터를 Map으로 어쩍 ㅜ할 때
근데 지금은 동기식방식이기 때문에 저걸 쓰면 안된다.
내부적을 ㅗjson형식으로 온다. 그래서 그걸 풀어서 담기게 해야함. 그걸 잭슨이 하는거고.
그걸 대체시ㅣㅋ는게 @RequestBody다.
- 결론. 비동기식은 @RequestBody를 붙인다. 동기식은 @RequestParam을 붙인다.
- Map이랑 String 변수랑 다 두면 Map에도 담기고 String 변수들에도 다 담김?
제일 좋은건 컨트롤러 메소드에 로그를 찍는다. @Slf4j 추가하고.
log.debug("map: {}", search);
log.debug("condition: {}, keyword: {}", condition, keyword);
- 다 담긴다.
ㅁ BoardServiceImpl
package com . br . spring . service ;
import java . util . List ;
import java . util . Map ;
import org . springframework . stereotype . Service ;
import com . br . spring . dao . BoardDao ;
import com . br . spring . dto . BoardDto ;
import com . br . spring . dto . PageInfoDto ;
import com . br . spring . dto . ReplyDto ;
import lombok . RequiredArgsConstructor ;
@ RequiredArgsConstructor
@ Service
public class BoardServiceImpl implements BoardService {
private final BoardDao boardDao ;
@ Override
public int selectBoardListCount () {
return boardDao . selectBoardListCount ();
}
@ Override
public List < BoardDto > selectBoardList ( PageInfoDto pi ) {
return boardDao . selectBoardList ( pi );
}
@ Override
public int selectSearchListCount ( Map < String , String > search ) {
return boardDao . selectSearchListCount ( search );
}
@ Override
public List < BoardDto > selectSearchList ( Map < String , String > search , PageInfoDto pi ) {
return boardDao . selectSearchList ( search , pi );
}
@ Override
public int insertBoard ( BoardDto b ) {
return 0 ;
}
@ Override
public int updateIncreaseCount ( int boardNo ) {
return 0 ;
}
@ Override
public BoardDto selectBoard ( int boardNo ) {
return null ;
}
@ Override
public int deleteBoard ( int boardNo ) {
return 0 ;
}
@ Override
public List < ReplyDto > selectReplyList ( int boardNo ) {
return null ;
}
@ Override
public int insertReply ( ReplyDto r ) {
return 0 ;
}
}
ㅁ BoardDao
package com . br . spring . dao ;
import java . util . List ;
import java . util . Map ;
import org . apache . ibatis . session . RowBounds ;
import org . mybatis . spring . SqlSessionTemplate ;
import org . springframework . stereotype . Repository ;
import com . br . spring . dto . BoardDto ;
import com . br . spring . dto . PageInfoDto ;
import lombok . RequiredArgsConstructor ;
@ RequiredArgsConstructor
@ Repository
public class BoardDao {
private final SqlSessionTemplate sqlSession ;
public int selectBoardListCount () {
return sqlSession . selectOne ( "boardMapper.selectBoardListCount" );
}
public List < BoardDto > selectBoardList ( PageInfoDto pi ) {
RowBounds rowBounds = new RowBounds (( pi . getCurrentPage () - 1 ) * pi . getBoardLimit (), pi . getBoardLimit ());
return sqlSession . selectList ( "boardMapper.selectBoardList" , null , rowBounds );
}
public int selectSearchListCount ( Map < String , String > search ) {
return sqlSession . selectOne ( "boardMapper.selectSearchListCount" , search );
}
public List < BoardDto > selectSearchList ( Map < String , String > search , PageInfoDto pi ) {
RowBounds rowBounds = new RowBounds (( pi . getCurrentPage () - 1 ) * pi . getBoardLimit (), pi . getBoardLimit ());
return sqlSession . selectList ( "boardMapper.selectSearchList" , search , rowBounds ); // 쿼리를 완성시키기 위해 2번째 인자값으로 map객체를 넘김.
}
}
- 검색결과에 만족하는 게시글의 갯수가 반환된다.
ㅁ board-mapper.xml
<? xml version = "1.0" encoding = "UTF-8" ?>
< mapper namespace = "boardMapper" >
< resultMap id = "boardResultMap" type = "BoardDto" >
< result column = "board_no" property = "boardNo" />
< result column = "board_title" property = "boardTitle" />
< result column = "user_id" property = "boardWriter" />
< result column = "count" property = "count" />
< result column = "regist_date" property = "registDt" />
< result column = "attach_count" property = "attachCount" />
</ resultMap >
< select id = "selectBoardListCount" resultType = "_int" >
select
count(*)
from board
where status = 'Y'
</ select >
< select id = "selectBoardList" resultMap = "boardResultMap" >
select
b.board_no
, board_title
, user_id
, count
, to_char(regist_date, 'YYYY-MM-DD') as "regist_date"
, (
select count(*)
from attachment
where ref_type = 'B'
and ref_no = b.board_no
) as "attach_count"
from board b
join member on (user_no = board_writer)
where b.status = 'Y'
order by board_no desc
</ select >
< select id = "selectSearchListCount" resultType = "_int" >
select
count(*)
from board b
join member on (user_no = board_writer)
where b.status = 'Y'
and ${condition} like '%' || #{keyword} || '%'
</ select >
< select id = "selectSearchList" resultMap = "boardResultMap" >
select
b.board_no
, board_title
, user_id
, count
, to_char(regist_date, 'YYYY-MM-DD') as "regist_date"
, (
select count(*)
from attachment
where ref_type = 'B'
and ref_no = b.board_no
) as "attach_count"
from board b
join member on (user_no = board_writer)
where b.status = 'Y'
and ${condition} like '%' || #{keyword} || '%'
order by board_no desc
</ select >
</ mapper >
- 마이바티스 때는 동적쿼리를 사용했었다.
넘어오는 condition이 3개 중 뭐냐에 따라서 if else처리를 했다. 그렇게 해도 된다. - 그런데 이번에는 동적쿼리를 사용하지 않으려고 요청시 전달값(검색조건)을 아예 테이블에 실재 존재하는 컬럼명으로 보냈다.
- board_title, board_content는 board 테이블의 컬럼이다. 그런데 user_id는 board 테이블이 아니라서 조인을 해줘야한다.
- #{ }는 그 타입에 맞춰서 값이 채워진다. 숫자면 따옴표 없이, 문자면 따옴표로 감싸서 값이 채워진다.
- 컬럼명에는 #{ }이 아니라 ${ }를 써야 한다. 실제 테이블 내에 존재하는 컬럼명을 지칭하려면 따옴표가 감싸지면 안된다.
오라클에서의 키워드, 테이블명, 컬럼명 등의 메타데이터로 값을 대체시키고자 한다면 ${ }로 값을 채운다.
그럼 어떤 타입이든 따옴표가 감싸지지 않은 채로 채워진다.
- 첫쿼리: 조인, 조건한줄추가, where 조건절의 status가 중복되어서 board에 b 별칭 부여.
- 두번째: 조건한줄추가
- 쿼리 자체는 검색 결과가 싹 다 조회된다.
이 중 사용자가 원하는 몇개 데이터만 조회해야한다. RowBounds를 쓰면 된다.
=========================================================
- 그런데 검색을 하면 페이징처리가 잘 되긴 하는데 2번, 3번 등의 페이지를 누르면 검색한게 풀린다.
- 현재 페이징 바는 무조건 list.do를 요청하고 있기 때문이다.
- 검색 후 list.jsp왔을 대는 search_form을 강제로 submit 시키면 된다. 단 내가 요청한 번호를 넘긴다.
- 검색후 결과에서 다른 페이지를 눌러도 search.do로 요청된다.
- 여기서 기본 이벤트 는 <a> 태그가 클릭될 때 발생하는 기본 동작 을 의미합니다. 보통 <a> 태그는 클릭 시 해당 링크의 href 속성에 지정된 URL로 페이지를 이동합니다. 이 동작을 막기 위해 return false를 사용하거나 event.preventDefault()를 호출할 수 있습니다.
즉, return false는 <a> 태그 클릭 시 발생하는 기본 페이지 이동 동작을 중단하고, 대신 자바스크립트로 정의된 기능(페이징 동작에 맞춰 search_form을 submit)만 실행되도록 합니다.
- 기본 이벤트 중단과 이벤트 전파 중단은 다릅니다.
기본 이벤트 중단은 요소 자체의 동작을 막는 것이고, 이벤트 전파 중단은 이벤트가 상위 요소로 전달되는 것을 막는 것입니다.
기본 이벤트 중단: 요소가 가지고 있는 고유의 기본 동작을 막는 것입니다. 예를 들어, <a> 태그를 클릭하면 링크로 이동하는 것이 기본 동작인데, 이를 막기 위해 event.preventDefault()나 return false를 사용합니다.
이벤트 전파 중단: 이벤트가 부모 요소로 전파되는 것을 막는 것입니다. 이벤트는 일반적으로 요소에서 발생하면 상위 부모 요소들로 계속 전파됩니다(버블링). 이 전파를 막기 위해 event.stopPropagation()을 사용합니다.
- 페이지 깜박거림없이 하고싶으면 지금까지 한 걸 ajax로 바꾸면된다.