1.검색할 대상을 뭐로 선택하느냐에 따라 쿼리가 달라지므로 동적쿼리
2.페이지 이동 처리
sql을 공통부분으로 만들고, include로 공통부분을 받아온다.
A = 내용 + 제목
T = 제목
W = 작성자
if문으로 하기엔 셋 다 조건에 맞을 수 있으므로
우리가 지금 개발하려는 기능에는 choose가 낫다
%를 와일드카드라고 하는데, 여러 글자를 뜻함(0+, 0이거나 더 많거나 - title도 되고 title1도 된다.)
_는 한글자를 뜻함 (title_일때, title은 안되고 title1은 된다.)
bno가 여러개일 때, foreach로 배열을 만들어서 구분자를 콤마로 해서 값들을 다 구성해주는 것
(where bno in (1,2,3) 이라는 sql절도 있지만 훨씬 더 많을 때 사용)
boardMapper.xml
<select id="selectPage" 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>
SearchCondition이라는 parameterType앞에 패키지명을 제외하려면,
mybatis-config.xml로 가서 수정해주어야 한다
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<typeAliases>
<typeAlias alias="BoardDto" type="com.fastcampus.ch4.domain.BoardDto"/>
<typeAlias alias="SearchCondition" type="com.fastcampus.ch4.domain.SearchCondition"/>
</typeAliases>
</configuration>
boardMapper.xml
<select id="searchResultCnt" parameterType="SearchCondition" resultType="int">
SELECT count(*)
FROM board
WHERE true
AND title LIKE concat('%', #{keyword}, '%')
</select>
기본적인 쿼리로 전부 다 작성 완료
BoardDaoImpl.java
Mapper에 SQL을 만들어놓았으니, 지금 만든 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);
}
위에 만든 두 메소드들을 복사해서
BoardDao.interface에 추가해준다.
int searchResultCnt(SearchCondition sc) throws Exception;
List<BoardDto> searchSelectPage(SearchCondition sc) throws Exception;
domain에
SearchCondition.java를 만든다.
검색 조건이므로
제공해야하는 것이 키워드,오프셋,페이지사이즈,옵션바다.
기본값을 주는 이유는 컨트롤러에서 값을 안 줬을때 기본값을 사용하기 위해서다.
package com.fastcampus.ch4.domain;
public class SearchCondition {
private Integer page=1;
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 offset;
}
public void setOffset(Integer offset) {
this.offset = offset;
}
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;
}
}
새로 SQL을 만들어줬으니 테스트를 한다.
BoardDaoImplTest.java
@Test
public void searchSelectPageTest() throws Exception{
boardDao.deleteAll();
for (int i=1; i<=20; i++){
BoardDto boardDto = new BoardDto("title"+i,"asdfasdf","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); //1~20중에 title2, title20이 나와야함
}
실제 실행되고 있는 sql을 보여주기 위해 다운받았다.
(아래는 설정 파일들)
log4jdbc.log4j2.properties
log4jdbc.spylogdelegator.name=net.sf.log4jdbc.log.slf4j.Slf4jSpyLogDelegator
logback.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE configuration>
<configuration>
<include resource="org/springframework/boot/logging/logback/base.xml"/>
<!-- log4jdbc-log4j2 -->
<logger name="jdbc.sqlonly" level="INFO"/>
<logger name="jdbc.sqltiming" level="INFO"/>
<logger name="jdbc.audit" level="WARN"/>
<logger name="jdbc.resultset" level="INFO"/>
<logger name="jdbc.resultsettable" level="INFO"/>
<logger name="jdbc.connection" level="INFO"/>
</configuration>
root-context.xml
<!-- Root Context: defines shared resources visible to all other web components -->
<bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
<property name="driverClassName" value="net.sf.log4jdbc.sql.jdbcapi.DriverSpy"></property>
<property name="url" value="jdbc:log4jdbc:mysql://localhost:3306/springbasic?useUnicode=true&characterEncoding=utf8"></property>
<!-- <property name="driverClassName" value="com.mysql.jdbc.Driver"></property>-->
<!-- <property name="url" value="jdbc:mysql://localhost:3306/springbasic?useUnicode=true&characterEncoding=utf8"></property>-->
<property name="username" value="hyerin"></property>
<property name="password" value="hyerin"></property>
</bean>
Service 만들기
BoardServiceImpl.java
@Override
public List<BoardDto> getSearchResultPage(SearchCondition sc) throws Exception{
return boardDao.searchSelectPage(sc);
}
@Override
public int getSearchResultCnt(SearchCondition sc) throws Exception{
return boardDao.searchResultCnt(sc);
}
BoardService.java
List<BoardDto> getSearchResultPage(SearchCondition sc) throws Exception;
int getSearchResultCnt(SearchCondition sc) throws Exception;
Service Test는 생략됨 ㅜ.ㅜ
Controller를 고친다
BoardController.java
@GetMapping("/list")
public String list(Integer page, Integer pageSize, Model m, HttpServletRequest request) {
if(!loginCheck(request))
return "redirect:/login/login?toURL="+request.getRequestURL(); // 로그인을 안했으면 로그인 화면으로 이동
if(page==null) page=1;
if(pageSize==null) pageSize=10;
try {
int totalCnt = boardService.getCount();
PageHandler pageHandler = new PageHandler(totalCnt,page,pageSize); //페이지 네비게이션 1
Map map = new HashMap();
map.put("offset", (page-1)*pageSize);
map.put("pageSize", pageSize);
List<BoardDto> list = boardService.getPage(map);
m.addAttribute("list", list);
m.addAttribute("ph",pageHandler); //페이지 네비게이션 2
m.addAttribute("page",page); //페이지 읽기 - 아래줄까지 추가
m.addAttribute("pageSize",pageSize);
} catch (Exception e) {
throw new RuntimeException(e);
}
return "boardList"; // 로그인을 한 상태이면, 게시판 화면으로 이동
}
아래와 같이 소스 고침
@GetMapping("/list")
public String list(@ModelAttribute SearchCondition sc, Model m, HttpServletRequest request) {
if(!loginCheck(request))
return "redirect:/login/login?toURL="+request.getRequestURL();
try {
int totalCnt = boardService.getSearchResultCnt(sc);
m.addAttribute("totalCnt",totalCnt);
PageHandler pageHandler = new PageHandler(totalCnt,sc);
List<BoardDto> list = boardService.getSearchResultPage(sc);
m.addAttribute("list", list);
m.addAttribute("ph",pageHandler);
} catch (Exception e) {
throw new RuntimeException(e);
}
return "boardList"; // 로그인을 한 상태이면, 게시판 화면으로 이동
}
PageHandler 우클릭 > Go to > Implementation
PageHandler.java
public class PageHandler {
// private int page; //현재 페이지
// private int pageSize; //한 페이지의 크기
// private String option;
// private String keyword;
private SearchCondition sc;
원래 페이지와 페이지 사이즈만 받았는데, 옵션과 키워드도 받았다.
4개를 한 개의 변수로 묶어준다 (sc로)
page랑 pageSize가 들어간 것들 다 없애버리기
package com.fastcampus.ch4.domain;
public class PageHandler {
// private int page; //현재 페이지
// private int pageSize; //한 페이지의 크기
// private String option;
// private String keyword;
private SearchCondition sc;
private int totalCnt; //총 게시물 개수
private int naviSize=10; //페이지 네비게이션의 크기
private int totalPage; //전체 페이지의 갯수
private int beginPage; //네비게이션의 첫번째 페이지
private int endPage; // 네비게이션의 마지막 페이지
private boolean showPrev; //이전 페이지로 이동하는 링크를 보여줄 것인지의 여부
private boolean showNext; //다음 페이지로 이동하는 링크를 보여줄 것인지의 여부
public PageHandler(int totalCnt, SearchCondition sc){
this.totalCnt=totalCnt;
this.sc=sc;
doPaging(totalCnt,sc);
}
public void doPaging(int totalCnt,SearchCondition sc){ //페이징 계산하는데 필요한 변수3개로 생성자 만들기
this.totalCnt=totalCnt;
totalPage = (int)Math.ceil(totalCnt/(double)sc.getPageSize()); //Math.ceil=올림, double로 나오므로 int화
beginPage = (sc.getPage()-1)/naviSize*naviSize+1; //일의 자릿수를 버리기
endPage = Math.min(beginPage + naviSize -1 ,totalPage);
showPrev=beginPage!=1; //beginPage가 1만 아니면 된다.(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;
}
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 +
'}';
}
}
SearchCondition.java에도 toString을 만들어주었다.
board.jsp와 boardList.jsp를 바꿔준다
그리고 SearchCondition.java에 코드를 추가해준다
public String getQueryString(Integer page){ //page를 받아서 해당 페이지에 대한 네비게이션을 한다.
return UriComponentsBuilder.newInstance()
.queryParam("page",page) //지정된 페이지로 세팅이 되도록
.queryParam("pageSize",pageSize)
.queryParam("option",option)
.queryParam("keyword",keyword)
.build().toString();
}
public String getQueryString(){
// //컨트롤러에서 매개변수를 받는것을 묶은 것이 SearchCondition인데, 내용을 받다가 목록으로 돌아갈 때 값을 유지해야 한다.
// //?page=1&pageSize=10&option=T&keyword="title"
// //위와 같이 queryString으로 줘야하는데 일일이 주기 번거로우므로 메소드를 만들었다.
// return UriComponentsBuilder.newInstance()
// .queryParam("page",sc.getPage())
// .queryParam("pageSize",sc.getPageSize())
// .queryParam("option",sc.getOption())
// .queryParam("keyword",sc.getKeyword())
// .build().toString();
return getQueryString(page);
//메소드가 중복되므로, 위 내용들을 주석처리해주고,
//페이지를 지정해주면 그 페이지를 쓰고, 페이지를 지정해주지않으면 갖다쓴다.
}
그리고 offset 변수를 없앤다.
setOffset 함수도 없앤다.
getOffset 함수만 남긴다.
// private Integer offset=0; //page와 pageSize로 계산되는 값이기 때문에 iv로 줄 필요가 없다
boardMapper.xml에서 제대로 쿼리를 작성해보자
<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>
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>
BoardDaoImplTest.java가 잘 돌아가는지 확인한다.
@Test
public void searchResultCntTest() throws Exception{
boardDao.deleteAll();
for (int i=1; i<=20; i++){
BoardDto boardDto = new BoardDto("title"+i,"asdfasdf","asdf"+i);
boardDao.insert(boardDto);
}
SearchCondition sc = new SearchCondition(1,10,"title2","T");
int cnt = boardDao.searchResultCnt(sc);
assertTrue(cnt==2); //1~20중에 title2, title20이 나와야함
sc = new SearchCondition(1,10,"asdf2","W");
cnt = boardDao.searchResultCnt(sc);
assertTrue(cnt==2); //1~20중에 asdf2,asdf20
}
@Test
public void searchSelectPageTest() throws Exception{
boardDao.deleteAll();
for (int i=1; i<=20; i++){
BoardDto boardDto = new BoardDto("title"+i,"asdfasdf","asdf"+i);
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); //1~20중에 title2, title20이 나와야함
sc = new SearchCondition(1,10,"asdf2","W");
list = boardDao.searchSelectPage(sc);
// System.out.println("list = " + list);
assertTrue(list.size()==2); //1~20중에 asdf2, asdf20이 나와야함
}
boardMapper.xml에 쿼리문이 중복이니, 별도의 sql문으로 만들어보자.
<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>
<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>
board.jsp
버전 문제 때문에
<%@taglib prefix="c" uri="http://java.sun.com/jstl/core_rt" %>
_rt를 붙여주었다
<input name="title" type="text" value="<c:out value='${boardDto.title}'/>" placeholder=" 제목을 입력해 주세요." ${mode=="new" ? "" : "readonly='readonly'"}><br>
<textarea name="content" rows="20" placeholder=" 내용을 입력해 주세요." ${mode=="new" ? "" : "readonly='readonly'"}><c:out value="${boardDto.content}"/></textarea><br>
제목이나 내용에 script를 넣지 않도록 미리 방지해주는 <c:out>태그
'패캠 챌린지' 카테고리의 다른 글
댓글 기능 구현 - back (0) | 2023.06.15 |
---|---|
REST API와 Ajax (1) | 2023.06.14 |
게시판 CRUD 기능 구현_읽기, 삭제 (0) | 2023.06.05 |
게시판 목록 만들기와 페이징 - TDD(2) (1) | 2023.05.24 |
게시판 목록 만들기와 페이징 - TDD(1) (1) | 2023.05.12 |