✔️ 동적쿼리
: 검색할 대상을 뭘로 선택하느냐에 따라서 쿼리가 달라져야 한다.
✔️ 페이지 이동처리
: 검색할 결과에서 어떤 내용을 읽었는데 목록보기를 눌렀을 때 페이징 처리도 신경써야 한다.
selected
✔️ 공통 부분을 <sql>로 정의하고 <include>로 포함시켜 재사용
우리가 계속 쓰던 if문이 맞다.
내용과 제목으로 검색할 때는 title하고 content에 OR로 연결해서 검색하게 한다.
제목으로 검색할 때는 title
작성자로 검색할 때는 writer
where 조건으로 true 쓴 것은 바로 AND가 오기 때문에 씀
: true가 없으면 바로 AND가 나오기 때문이다.
: 조건이 여러개 지정해야 되기 때문에 AND를 뺄 수가 없다. 여러개 연결할 때는 AND가 붙어야 한다. (OR도 마찬가지) 그래서 true를 붙이고 시작한다.
하지만 이건 적합하지 않다. if문은 이 3개 중에 여러개가 조건에 맞을 수 있다. 하지만 우리는 3개 중에 하나만 조건에 맞아야 한다. 그래서 if문 보다는 choose가 더 잘 어울린다.
if-elseif문과 비슷하다.
test='option==T'
이면 밑에 test='option=='W'
는 검사하지 않는다.
➡️ 효율적임
: 항상 그런 것은 아니고, 검색 기능에서 이 두개의 옵션이 동시에 들어올 수 없기 때문에 choose가 적합하다는 것
위 두 옵션 둘다 false이면 <otherwise>가 동작된다.
: <otherwise> = else 같은 느낌
WHERE bno in(1,2,3)
/ WHERE bno = 1
이런식은 값을 알았을 때 쓸 수 있지만 값이 많을 때는 이렇게 쓰기 어렵다. 그럴 때 사용하는 것이 <foreach> 태그 이다.
for문 써서 배열로 주면 괄호 안에다가 콤마를 구분자로 해서 만들어 준다.
➡️ WHERE bno in(1,2,3)
이렇게 똑같이 만들어 준다.
배열을 받아서 넘겨줘야 처리 가능
✔️ 검색 조건들이 모여있는 SerachCondition 객체 생성
package kr.ac.jipark09.domain;
import org.springframework.web.util.UriComponentsBuilder;
// 검색 조건
public class SearchCondition {
// boardMapper.xml 검색 sql문 보고 들어갈 값 변수에 놓기
private Integer page = 1; // Controller에서도 쓸 것이기 때문에 넣어준다.
private Integer pageSize = 10;
// private Integer offset = 0;
private String keyword = "";
private String option = "";
public SearchCondition() {};
public SearchCondition(Integer page, Integer pageSize, String keyword, String option) {
this.page = page;
this.pageSize = pageSize;
this.keyword = keyword;
this.option = option;
}
public Integer getPage() {
return page;
}
public void setPage(Integer page) {
this.page = page;
}
public Integer getPageSize() {
return pageSize;
}
public void setPageSize(Integer pageSize) {
this.pageSize = pageSize;
}
public Integer getOffset() {
return (page - 1) * pageSize;
}
public String getKeyword() {
return keyword;
}
public void setKeyword(String keyword) {
this.keyword = keyword;
}
public String getOption() {
return option;
}
public void setOption(String option) {
this.option = option;
}
@Override
public String toString() {
return "SearchCondition{" +
"page=" + page +
", pageSize=" + pageSize +
", offset=" + getOffset() +
", keyword='" + keyword + '\'' +
", option='" + option + '\'' +
'}';
}
}
#{offset}
은 SearchCondition의 getter를 사용한다.✔️ mapper에 검색에 쓸 sql문 작성
<!-- 검색에 쓸 sql-->
<select id="searchSelectPage" parameterType="SearchCondition" resultType="BoardDto">
SELECT bno, title, content, writer, view_cnt, comment_cnt, reg_date
FROM board
WHERE true
AND title LIKE concat('%', #{keyword}, '%')
ORDER BY reg_date DESC , bno DESC
LIMIT #{offset}, #{pageSize}
</select>
<!--검색 결과가 몇개가 나왔는지 알아야 페이징을 할 수 있음-->
<select id="searchResultCnt" parameterType="SearchCondition" resultType="BoardDto">
SELECT count(*)
FROM board
WHERE true
AND title LIKE concat('%', #{keyword}, '%')
ORDER BY reg_date DESC , bno DESC
LIMIT #{offset}, #{pageSize}
</select>
SearchCondition
은 mybatis.config.xml에 alias 등록해 줘야 패키지명 다 안쓰게 할 수 있다.✔️ BoardDaoImpl에다가 sql문을 호출하는 메서드를 작성
@Override
public int searchResultCnt(SearchCondition sc) throws Exception {
return session.selectOne(namespace + "searchResultCnt", sc);
}
@Override
public List<BoardDto> searchSelectPage(SearchCondition sc) throws Exception {
return session.selectList(namespace + "searchSelectPage", sc);
}
✔️ test 하기
@Test
public void searchSelectPageTest() throws Exception {
boardDao.deleteAll();
for (int i = 1; i <= 20; i++) {
BoardDto boardDto = new BoardDto("title" + i, "asfadf", "asdf");
boardDao.insert(boardDto);
}
SearchCondition sc = new SearchCondition(1, 10, "title2", "T");
List<BoardDto> list = boardDao.searchSelectPage(sc);
System.out.println("list=" + list);
assertTrue(list.size() == 2);
}
@Test
public void searchResultCntTest() throws Exception {
boardDao.deleteAll();
for (int i = 1; i <= 20; i++) {
BoardDto boardDto = new BoardDto("title" + i, "asfadf", "asdf");
boardDao.insert(boardDto);
}
SearchCondition sc = new SearchCondition(1, 10, "title2", "T");
int result = boardDao.searchResultCnt(sc);
assertTrue(result == 2);
}
✔️ BoardServiceImpl 추가
@Override
public List<BoardDto> getSearchSelectPage(SearchCondition sc) throws Exception {
return boardDao.searchSelectPage(sc);
}
@Override
public int getSearchResultCnt(SearchCondition sc) throws Exception {
return boardDao.searchResultCnt(sc);
}
✔️ BoardController 수정:
@GetMapping("/list")
public String list(@RequestParam(defaultValue = "1") Integer page,
@RequestParam(defaultValue = "10") Integer pageSize,
String option, String keyword, Model model, HttpServletRequest request) {
if(!loginCheck(request))
return "redirect:/login/login?toURL=" + request.getRequestURL(); // 로그인을 안했으면 로그인 화면으로 이동
try {
// pageHandler 활용
int totalCnt = boardService.getCount();
PageHandler pageHandler = new PageHandler(totalCnt, page, pageSize);
if(page < 0 || page > pageHandler.getTotalPage()) {
page = 1;
} if(pageSize < 0 || pageSize > 20) {
pageSize = 10;
}
// page, pageSize를 파라미터로 받아서 offset과 pageSize를 map에 저장
Map map = new HashMap();
map.put("offset", (page - 1) * pageSize);
map.put("pageSize", pageSize);
List<BoardDto> list = boardService.getPage(map);
model.addAttribute("list", list); // jsp로 보냄
model.addAttribute("ph", pageHandler); // jsp에서 pageHandler를 가지고 페이지를 나타냄
model.addAttribute("page", page);
model.addAttribute("pageSize", pageSize);
} catch (Exception e) {
e.printStackTrace();
}
return "boardList"; // 로그인을 한 상태이면, 게시판 화면으로 이동
}
SerachCondition
으로 묶은 큰 이유 @GetMapping("/list")
public String list(SearchCondition sc, Model model, HttpServletRequest request) {
if(!loginCheck(request))
return "redirect:/login/login?toURL=" + request.getRequestURL(); // 로그인을 안했으면 로그인 화면으로 이동
try {
// pageHandler 활용
int totalCnt = boardService.getSearchResultCnt(sc);
model.addAttribute("totalCnt", totalCnt);
PageHandler pageHandler = new PageHandler(totalCnt, sc);
List<BoardDto> list = boardService.getSearchSelectPage(sc);
model.addAttribute("list", list); // jsp로 보냄
model.addAttribute("ph", pageHandler); // jsp에서 pageHandler를 가지고 페이지를 나타냄
} catch (Exception e) {
e.printStackTrace();
}
return "boardList"; // 로그인을 한 상태이면, 게시판 화면으로 이동
}
@ModelAttribute
가 생략되어 있다.✔️ PageHandler 수정: SerachCondition으로 묶었으니 바꿔줘야 함
public class PageHandler {
private int totalCnt; // 총 게시물 갯수
private int pageSize; // 한 페이지의 크기
private int naviSize = 10; // 페이지 내비게이션의 크기
private int totalPage; // 전체 페이지의 갯수
private int page; // 현재 페이지
private int beginPage; // 내비게이션의 첫 번째 페이지
private int endPage; // 내비게이션의 마지막 페이지
private boolean showPrev; // 이전 페이지로 이동하는 링크를 보여줄 것인지의 여부 (1페이지만 있을 때는 있으면 안되니까)
private boolean showNext; // 다음 페이지로 이동하는 링크를 보여줄 것인지의 여부 (10의 배수로 채워지지 않았을 때 있으면 안되니까)
// totalCnt와 page만 받아울 경우 pageSize는 기본 10으로 설정
public PageHandler(int totalCnt, int page) {
this(totalCnt, page, 10);
}
// 계산을 해서 페이지에 보여줄 화면을 구성
public PageHandler(int totalCnt, int page, int pageSize) {
this.totalCnt = totalCnt;
this.page = page;
this.pageSize = pageSize;
// totalPage = 전체게시물 / 10 (올림) => pageSize 정수 주의!!! 정수랑 정수랑 나눠서 소수점 x
totalPage = (int)Math.ceil(totalCnt / (double)pageSize);
// 25 / 10 * 10 + 1 = 11
// (page - 1) => 10 / 10 * 10 + 1 = 11되버림. 10의 자리는 그 따음페이지로 넘어가면 안됨. 10의 자리때문에 -1 해줘야함
beginPage = (page - 1) / naviSize * 10 + 1;
endPage = Math.min(beginPage + naviSize - 1, totalPage);
showPrev = beginPage != 1;
showNext = endPage != totalPage;
}
...
package kr.ac.jipark09.domain;
public class PageHandler {
// 검색
private SearchCondition sc;
// private int page; // 현재 페이지
// private int pageSize; // 한 페이지의 크기
// private String option;
// private String keyword;
private int totalCnt; // 총 게시물 갯수
private int naviSize = 10; // 페이지 내비게이션의 크기
private int totalPage; // 전체 페이지의 갯수
private int beginPage; // 내비게이션의 첫 번째 페이지
private int endPage; // 내비게이션의 마지막 페이지
private boolean showPrev; // 이전 페이지로 이동하는 링크를 보여줄 것인지의 여부 (1페이지만 있을 때는 있으면 안되니까)
private boolean showNext; // 다음 페이지로 이동하는 링크를 보여줄 것인지의 여부 (10의 배수로 채워지지 않았을 때 있으면 안되니까)
public PageHandler(int totalCnt, SearchCondition sc) {
setTotalCnt(totalCnt);
this.sc = sc;
doPaging(totalCnt, sc);
}
// 계산을 해서 페이지에 보여줄 화면을 구성
public void doPaging(int totalCnt, SearchCondition sc) {
this.totalCnt = totalCnt;
totalPage = (int)Math.ceil(totalCnt / (double)sc.getPageSize());
beginPage = (sc.getPage() - 1) / naviSize * 10 + 1;
endPage = Math.min(beginPage + naviSize - 1, totalPage);
showPrev = beginPage != 1;
showNext = endPage != totalPage;
}
public int getTotalCnt() {
return totalCnt;
}
public void setTotalCnt(int totalCnt) {
this.totalCnt = totalCnt;
}
public int getNaviSize() {
return naviSize;
}
public void setNaviSize(int naviSize) {
this.naviSize = naviSize;
}
public int getTotalPage() {
return totalPage;
}
public void setTotalPage(int totalPage) {
this.totalPage = totalPage;
}
public int getBeginPage() {
return beginPage;
}
public void setBeginPage(int beginPage) {
this.beginPage = beginPage;
}
public int getEndPage() {
return endPage;
}
public void setEndPage(int endPage) {
this.endPage = endPage;
}
public boolean isShowPrev() {
return showPrev;
}
public void setShowPrev(boolean showPrev) {
this.showPrev = showPrev;
}
public boolean isShowNext() {
return showNext;
}
public void setShowNext(boolean showNext) {
this.showNext = showNext;
}
// 페이지 네비게이션을 프린트하는 메서드
public void print() {
System.out.println("page = " + sc.getPage());
System.out.print(showPrev ? "[PREV] " : "");
for(int i = beginPage; i <= endPage; i++) {
System.out.print(i + " ");
}
System.out.println(showNext ? "[NEXT]" : "");
}
@Override
public String toString() {
return "PageHandler{" +
"sc=" + sc +
", totalCnt=" + totalCnt +
", naviSize=" + naviSize +
", totalPage=" + totalPage +
", beginPage=" + beginPage +
", endPage=" + endPage +
", showPrev=" + showPrev +
", showNext=" + showNext +
'}';
}
}
doPaging()
메서드로 바꿈✔️ SearchCondition에 쿼리스트링 자동완성 메서드를 만듬
// 페이지를 줘서 해당 페이지에 대한 네비게이션도 해줘야 함
public String getQueryString(Integer page) {
// ?page=1&pageSize=10&option=T&keyword="title" -> 일일이 치기 귀찮음 이렇게 만들어주는 메서드를 만들어줌
return UriComponentsBuilder.newInstance()
.queryParam("page", page)
.queryParam("pageSize", pageSize)
.queryParam("option", option)
.queryParam("keyword", keyword)
.build().toString();
}
// 쿼리스트링 처리 부분
public String getQueryString() {
return getQueryString(page);
}
getQueryString()
✔️ BoardMapper의 쿼리 수정 ➡️ 동적 쿼리로 바꾸기
<!-- 검색에 쓸 sql-->
<select id="searchSelectPage" parameterType="SearchCondition" resultType="BoardDto">
SELECT bno, title, content, writer, view_cnt, comment_cnt, reg_date
FROM board
WHERE true
<choose>
<when test='option == "T"'>
AND title LIKE concat('%', #{keyword}, '%')
</when>
<when test='option == "W"'>
AND writer LIKE concat('%', #{keyword}, '%')
</when>
<otherwise>
AND (title LIKE concat('%', #{keyword}, '%')
OR content LIKE concat('%', #{keyword}, '%'))
</otherwise>
</choose>
AND title LIKE concat('%', #{keyword}, '%')
ORDER BY reg_date DESC , bno DESC
LIMIT #{offset}, #{pageSize}
</select>
<!--검색 결과가 몇개가 나왔는지 알아야 페이징을 할 수 있음-->
<select id="searchResultCnt" parameterType="SearchCondition" resultType="int">
SELECT count(*)
FROM board
WHERE true
<choose>
<when test='option == "T"'>
AND title LIKE concat('%', #{keyword}, '%')
</when>
<when test='option == "W"'>
AND writer LIKE concat('%', #{keyword}, '%')
</when>
<otherwise>
AND (title LIKE concat('%', #{keyword}, '%')
OR content LIKE concat('%', #{keyword}, '%'))
</otherwise>
</choose>
</select>
✔️ 중복 sql문 수정: <sql><input>
<!--중복제거 -->
<sql id ="searchCondition">
<choose>
<when test='option == "T"'>
AND title LIKE concat('%', #{keyword}, '%')
</when>
<when test='option == "W"'>
AND writer LIKE concat('%', #{keyword}, '%')
</when>
<otherwise>
AND (title LIKE concat('%', #{keyword}, '%')
OR content LIKE concat('%', #{keyword}, '%'))
</otherwise>
</choose>
</sql>
<!-- 검색에 쓸 sql-->
<select id="searchSelectPage" parameterType="SearchCondition" resultType="BoardDto">
SELECT bno, title, content, writer, view_cnt, comment_cnt, reg_date
FROM board
WHERE true
<include refid="searchCondition"/>
ORDER BY reg_date DESC , bno DESC
LIMIT #{offset}, #{pageSize}
</select>
<!--검색 결과가 몇개가 나왔는지 알아야 페이징을 할 수 있음-->
<select id="searchResultCnt" parameterType="SearchCondition" resultType="int">
SELECT count(*)
FROM board
WHERE true
<include refid="searchCondition"/>
</select>
게시판 글쓰기에서 자바스크립트 코드를 넣으면 실행이 된다. 잘못해서는 보안에 우려가 생긴다. 이를 방지하기 위해 out태그를 사용한다.
<c:out value=' ${boardDto.title}'/>
<c:out value=" ${boardDto.content}"/>
Reference
: https://fastcampus.co.kr/dev_academy_nks