20220901_thu
실습내용
- 게시판 검색기능 구현 ( 제목/ 작성자 select 박스 추가시)
- 검색어 대소문자 구분 않고 검색기능
- 검색 후, 검색어 초기화시키지않고 저장
- 페이징 기능 구현
목록조회
- 쿼리문의 if문안에 있는 글자는 mybatis 에서 title 그대로 작성하면 getter 호출한다.
- 왜? BoardVO에 있는 getter를 호출하기 때문에! -> boardVO.getTitle(), boardVO.getSearchValue() 호출된다.
- _parameter :빈값이 하나만 있을 때, 이 하나의 빈값채우기 위해서 넘어오는 데이터로 대신 사용하는 것.
- 하지만 검색어기능이 추가됨으로써 이제는 searchKeyword값과 searchValue값 총 두개의 빈 값이 있기 때문에 parameter값을 사용하지 않는다!
- 쿼리문 내에서 컬럼명을 불러올 때는 홀따옴표가 붙지않은채로 들어와야한다.(ex. TITLE = #{})
- 그래서 이를 콘솔창으로 확인시,
#{}으로 사용하면 홀따옴표가 붙어 문자로 사용되고, ${}사용하면 홀따옴표가 붙지않아 데이터값으로 사용될 수 있다!(유의!)검색어 대소문자 구분방지: LOWER(),UPPER()
WHERE LOWER(${searchKeyword}) LIKE '%'||LOWER(#{searchValue})||'%'
<!-- 목록조회 -->
<select id="selectBoardList" resultMap="board">
SELECT BOARD_NUM
, TITLE
, WRITER
, CREATE_DATE
FROM SPRING_BOARD
<if test="searchValue != null and !searchValue.equals('')">
WHERE LOWER(${searchKeyword}) LIKE '%'||LOWER(#{searchValue})||'%'
</if>
ORDER BY BOARD_NUM DESC
</select>
클래스 상속
- 단순히 쿼리문에 빈값 두개라고 boardVO 만 던져주면 안된다.
- 매퍼 쿼리문에서 빈값들은 boardvo.getSeeacrKeyword(),boardvo.getSeearchValue() 호출한다.
- 그런데 이전 BoardVO 클래스에는 위의 검색어 변수들이 없으므로 따로 만들어줘야한다!
- 그래서 SearchVO 생성 대신 상속기능 해야한다!!!
- SearchVO도 마치 BoardVO 인 것처럼 사용할 수 있도록 상속한다
public class BoardVO extends SearchVO {}
package kh.study.board.vo;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
@Getter
@Setter
@ToString
public class BoardVO extends SearchVO {
private int boardNum;
private String title;
private String content;
private String writer;
private String createDate;
}
package kh.study.board.vo;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
@Getter
@Setter
@ToString
public class SearchVO {//상속 -> boardVO
private String searchKeyword;
private String searchValue;
}
//게시글 목록조회
List<BoardVO> selectBoardList(BoardVO boardVO);
//게시글 목록조회
@Override
public List<BoardVO> selectBoardList(BoardVO boardVO) {
return sqlSession.selectList("boardMapper.selectBoardList",boardVO);
}
@RequestMapping("/list")
: getMapping,postMapping 모두 받는 어노테이션을 사용한다.- 검색어/검색키워드 값이 null값 뜰 때와 빈값 뜰 때 차이점 구별하는 방법
- 주소창경로를 바로 list로 간 경우는 null
- 검색기능에 빈값으로 검색버튼 클릭시 '' 빈값
//게시글 목록 페이지로 이동
@RequestMapping("/list")
public String selectBoardList(Model model,BoardVO boardVO) {
//1-2.게시글 목록 조회(한줄요약)
model.addAttribute("boardList",boardService.selectBoardList(boardVO));
return "board/board_list";//board_list.html
}
검색기능
- 넘어가는 value값은 쿼리문에 들어가는 빈값들이 컬럼명으로 들어가는 것과 같다
- 검색버튼 클릭시,searchKeyword는 둘 중 하나를 선택하는 것이기때문에 무조건 데이터값이 넘어간다 빈값이나 null값이 뜰 수가 없다
- 단, 기본경로 주소창 입력시에는 검색버튼을 누른 것이아니기 때문에 null값이 뜬다
- select에서만 사용가능한 타임리프 if문 사용
th:selected="${boardVO.searchKeyword == 'TITLE'}"
:선택한 키워드로 검색했을 때, ''값과 일치하면 selected(기본값)으로 선택한 키워드가 저장되어 목록페이지에 뜨도록 설정하겠다.- 검색버튼 누르면 name값 searchValue값이 넘어간다
- 그런데 검색어가 아무것도 없는 빈값이면 빈값으로 넘어간다 null이아니다!
- 단, 기본경로 주소창 입력시에는 검색버튼을 누른 것이아니기 때문에 null값이 뜬다
- th:value 타임리프적용시, 검색어 검색창에 그대로 저장 커맨드객체때문에 데이터를 넘기지않아도 자동으로 데이터가 넘어간다.
- 넘어갈때는 클래스명에서 소문자로바꿔 자동으로 넘어간다:
${boardVO.searchValue}
<!-- 검색기능 구현 -->
<form action="/board/list" method="post">
<select name="searchKeyword">
<option value="TITLE" th:selected="${boardVO.searchKeyword == 'TITLE'}">제목</option>
<option value="WRITER" th:selected="${boardVO.searchKeyword == 'WRITER'}">작성자</option>
</select>
<input type="text" name="searchValue" th:value="${boardVO.searchValue}">
<input type="submit" value="검색"><br>
</form>
Math.ceil()
: 실수로 반올림 연산기능public class PageVO extends SearchVO
상속수정상속은 하나만 가능하다!
두개이상은 자바에서 안된다.
searchVO와 PageVO도 마치 boardVO 인 것처럼 사용할 수 있도록 상속한다
public class BoardVO extends PageVO
@Getter
@Setter
@ToString
//상속은 하나만 가능하다! 두개이상은 자바에서 안된다!!!
public class BoardVO extends PageVO {// searchVO도 마치 boardVO 인 것처럼 사용할 수 있도록 상속한다
//lombok 어노테이션(위에) 을 이용해 getter,setter를 편하게 만들수있다
private int boardNum;
private String title;
private String content;
private String writer;
private String createDate;
}
BoardVO > PageVO > SearchVO 순서 상속된다.
public class PageVO extends SearchVO
package kh.study.board.vo;
public class PageVO extends SearchVO {//연달아 상속받도록!BoardVO > PageVo > SearchVO
private int nowPage;//현재선택된페이지
private int totalDataCnt;//전체게시글(데이터)수
private int beginPage;//화면에 보이는 첫 페이지
private int endPage;//화면에 보이는 마지막 페이지
private int displayCnt;//한 화면에 보여지는 게시글 수
private int displayPageCnt;//한 화면에 보여지는 페이지 수
private boolean prev;//이전 버튼의 유무
private boolean next;//다음 버튼의 유무
private int startNum;//시작 row_num 행번호
private int endNum;//마지막 row_num 행번호
//생성자(객체만들어지면서 시작되는 페이지 기본설정)
public PageVO() {
nowPage = 1;
displayCnt = 10;
displayPageCnt = 10;
}
public void setPageInfo() {//페이지정보 setter
//화면에 보이는 마지막 페이지 번호
endPage = displayPageCnt * (int)Math.ceil(nowPage/(double)displayPageCnt);
//시작페이지
beginPage = endPage-displayPageCnt+1;
//전체 페이지수
int totalPageCnt = (int)Math.ceil(totalDataCnt / (double)displayCnt);
//next 버튼 유무
if(endPage < totalPageCnt) {//화면에 보이는 마지막 페이지값이 전체페이지 값(마지막페이지값)보다 작으면 next버튼보임
next=true;
}
else {
next=false;
endPage= totalPageCnt;//뒤에 페이지가 없으면 전체페이지까지만 보이도록!
}
//prev 유무( 삼항연산자 )
prev = beginPage ==1? false :true;
startNum = (nowPage-1) * displayCnt +1;
endNum = nowPage * displayCnt ;
}
public void setNowPage(int nowPage) {
this.nowPage =nowPage;
}
public int getNowPage() {
return nowPage;
}
public void setTotalDataCnt(int totalDataCnt) {
this.totalDataCnt = totalDataCnt;
}
public int getTotalDataCnt() {
return totalDataCnt;
}
public void setNext(boolean next) {
this.next = next;
}
public boolean getNext() {
return next;
}
public void setPrev(boolean prev) {
this.prev = prev;
}
public boolean getPrev() {
return prev;
}
public int getBeginPage() {
return beginPage;
}
public int getEndPage() {
return endPage;
}
public int getStartNum() {
return startNum;
}
public int getEndNum() {
return endNum;
}
}
ROWNUM 은 조회된 행번호를 부여한다.
- 문제점은 한번 조회후, 다른 값으로 조회시 순서가 바뀌어 오류발생.
- 처음부터 모든 게시글을 조회 후, rownum 행번호를 조회하고 게시글번호 내림차순(최신순)으로 조회하는 순서로 진행해야 문제점이 발생하지 않는다!
- 그래서 3단 서브쿼리를 사용하여 해결한다.
- 단, 첫번째 조회쿼리문에서 ROWNUM과 달리 두번째,세번째 쿼리문에서는 별칭을 사용해 ROW_NUM을 사용해야한다!!!
-- 게시글 목록 조회
SELECT BOARD_NUM , TITLE , WRITER , CREATE_DATE
FROM SPRING_BOARD;
-- 페이징처리
-- 한 페이지에 10개 게시글만 조회
-- ROWNUM : 조회된 행 번호
SELECT ROWNUM , BOARD_NUM , TITLE , WRITER , CREATE_DATE
FROM SPRING_BOARD;
-- ROWNUM이용해서 1-10까지 조회
SELECT ROWNUM , BOARD_NUM , TITLE , WRITER , CREATE_DATE
FROM SPRING_BOARD
WHERE ROWNUM >= 1 AND ROWNUM <=10;
--ROWNUM 문제점발생: 만약 다시 11-20을 조회하면 순서가 뒤죽박죽 섞여버려 오류발생
SELECT ROWNUM , BOARD_NUM , TITLE , WRITER , CREATE_DATE
FROM SPRING_BOARD
ORDER BY TITLE;
-- 문제점 해결 서브쿼리 작성
--예시
SELECT TITLE, CONTENT
FROM SPRING_BOARD
WHERE BOARD_NUM < 10;
-- FROM 절 안에 테이블명 대신 위에 쿼리문으로 서브쿼리 작성
-- 해석: 서브쿼리로 조회된 데이터 결과값 중에서 조회하겠다
SELECT AAA
FROM
(
SELECT TITLE AS AAA, CONTENT
FROM SPRING_BOARD
WHERE BOARD_NUM < 10
)
WHERE AAA IS NOT NULL;
-- 먼저 서브쿼리에서 ROWNUM 없이 게시글번호 내림차순으로 조회한 값에서
-- 내림차순이면 최신글부터 보일 수 있도록 조회
-- 이후 ROWNUM 붙여 조회
SELECT ROWNUM
, BOARD_NUM
, TITLE
, WRITER
, CREATE_DATE
FROM
(
SELECT BOARD_NUM , TITLE , WRITER , CREATE_DATE
FROM SPRING_BOARD
ORDER BY BOARD_NUM DESC
)
WHERE ROWNUM >=11 AND ROWNUM <= 20;
-- 하지만 여전히 문제점 그대로 발생
-- 서브쿼리가 전체조회한 후 조건보는게 아니라
-- 한줄씩 조회하고 조건문 조회하다보니 ROWNUM 이 계속 1만 생기고 결국 조회안됨
-- 그래서 순서를 바꿔서 다시 서브쿼리 작성(3단 쿼리)
SELECT ROW_NUM-- FROM절에 있는 ROW_NUM 값
, BOARD_NUM
, TITLE
, WRITER
, CREATE_DATE
FROM
(
SELECT ROWNUM AS ROW_NUM
, BOARD_NUM
, TITLE
, WRITER
, CREATE_DATE
FROM
(
SELECT BOARD_NUM , TITLE , WRITER , CREATE_DATE
FROM SPRING_BOARD
ORDER BY BOARD_NUM DESC
)--여기 쿼리문은 조건문 없어서 모든 데이터의 ROWNUM을 조회하게됨
)
-- 미리 싹 다 조회된 ROWNUM에서 아래조건문대로 조회하겠다.
WHERE ROW_NUM >=11 AND ROW_NUM <=20;
WHERE ROW_NUM >= #{startNum} AND ROW_NUM <= #{endNum}
에서
- '~보다 크거나 같다' : >= 대신 > 을 사용한다.
- '~보다 작거나 같다' : <= 대신 < 을 사용한다.
- 목록조회쿼리실행을 위해 전체 게시글(데이터) 수 조회 쿼리문을 새로 만들어줘야한다.
<!-- boardVO 안에 빈값이 총 4개값이 되었다.
startNum,endNum 값은 boardVO에 없고 pageVO에 있기 때문에 상속기능! -->
<select id="selectBoardList" resultMap="board">
SELECT ROW_NUM
, BOARD_NUM
, TITLE
, WRITER
, CREATE_DATE
FROM
(
SELECT ROWNUM AS ROW_NUM
, BOARD_NUM
, TITLE
, WRITER
, CREATE_DATE
FROM
(
SELECT BOARD_NUM
, TITLE
, WRITER
, CREATE_DATE
FROM SPRING_BOARD
<if test="searchValue != null and !searchValue.equals('')">
WHERE LOWER(${searchKeyword}) LIKE '%'||LOWER(#{searchValue})||'%'
</if>
ORDER BY BOARD_NUM DESC
)
)
WHERE ROW_NUM >= #{startNum} AND ROW_NUM <= #{endNum}
</select>
<!-- 전체 데이터(게시글) 수 조회 -->
<select id="selectBoardCnt" resultType="int">
SELECT COUNT(BOARD_NUM)
FROM SPRING_BOARD
</select>
페이징 처리 위해 추가된 사항
- mapper에서 만든 selectBoardCnt() 메소드사용해서 전체데이터(게시글) 수 먼저 값 가져오기
int totalCnt = boardService.selectBoardCnt();
- 위에서 만든 데이터값 넣어주기.
boardVO.setTotalDataCnt(totalCnt);
- (목록조회 전에 미리) 페이지정보 세팅
boardVO.setPageInfo();
//게시글 목록 페이지로 이동
@RequestMapping("/list")//get,post 모두 받는 어노테이션
public String selectBoardList(Model model,BoardVO boardVO) {
//전체 데이커(게시글) 수를 먼저 가져오기
//이때문에 mapper에서 또 조회쿼리문 생성한것
int totalCnt = boardService.selectBoardCnt();
//db에서 쿼리문 메소드기능 실행한 값(totalCnt)을 totalDataCnt에 넣어주기
boardVO.setTotalDataCnt(totalCnt);
//페이지 정보 세팅(목록조회전)
boardVO.setPageInfo();
model.addAttribute("boardList",boardService.selectBoardList(boardVO));
return "board/board_list";//어디로 이동할래?
}
페이지 기능구현
- 테이블 밑에
<div>
태그로 작성- 페이징 예시
1 2 3 ...8 9 10 next
prev 11 12 13 14 ..19 20 next- 타임리프
<a>
태그 사용하여 prev, next,nowPage 페이지이동 버튼기능구현<th:block th:each="pageNum : ${#numbers.sequence(boardVO.beginPage, boardVO.endPage)}">
: beginPage부터 endPage까지 pageNum숫자를 하나씩 뽑겠다.
<!-- 페이징 기능 구현 -->
<div align="center">
<th:block th:if="${boardVO.prev}">
<a th:text="prev" th:href="@{/board/list(nowPage=${boardVO.beginPage-1})}"></a>
</th:block>
<th:block th:each="pageNum : ${#numbers.sequence(boardVO.beginPage, boardVO.endPage)}">
<a th:text="${pageNum}" th:href="@{/board/list(nowPage=${pageNum})}"></a>
</th:block>
<th:block th:if="${boardVO.next}">
<a th:text="next" th:href="@{/board/list(nowPage=${boardVO.endPage+1})}"></a>
</th:block>
</div>
<결과>
http://localhost:8081/board/list
- 각 페이지마다 10개의 게시글이 보이며, 화면에서 보이는 페이지의 수 는 10개씩 보이도록 구현(페이지정보셋팅 setPageInfo())
- 다른 페이지 숫자 클릭시, 해당 페이지 10개씩 게시글 출력
- 시작페이지 1이 보일 때는 pre 버튼 보이지 않기
- 마지막 페이지 26일 때는 나머지 27~30 페이지는 보이지 않고 next 버튼도 보이지 않도록 구현