✅ 검색할 대상을 뭘로 할지에 따라 쿼리 달리짐(동적쿼리)
✅ 게시글 페이지에서 목록을 눌렀을 때 게시글이 있는 목록 페이지으로 이동
공통 부분을 <sql>로 정의하고 <include>로 포함시켜 재사용
✅ %
(mysql/Oracle) : 여러글자(0~n)
✅ _
/ ?
(mysql/Oracle) : 한글자(1)
ex) 'title%' : title (O) | title1 (O)
'title_' : title (X) | title1 (O)
select * from board
↪ where true and titlelike
concat('title1', '%'); -- title%
↪ where true and titlelike
concat('title1', ''); -- title
↪ where true and titlenot like
concat('title1', ''); -- title 빼고
✔️ 한 개일 때 WHERE bno = 1
✔️ 여러 개일 때 WHERE bno in (1, 2, 3)
✔️ 개수가 정해져 있지 않은 경우 foreach
사용
select * from board
↪ where true and bnoin
(990, 991, 992)
↪ where true and bnonot in
(990, 991, 992)
order by bno;
✔️ script를 넣지 못하도록 title과 content는 out
태그처리
<c:out value="${boardDto.title}" />
<c:out value="${boardDto.content}"/>
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.option = option;
this.keyword = keyword;
this.page = page;
this.pageSize = pageSize;
}
public String getQueryString() {
return getQueryString(page);
}
public String getQueryString(Integer page) {
// ?page=10&pageSize=10&option=A&keyword=title
return UriComponentsBuilder.newInstance()
.queryParam("page", page)
.queryParam("pageSize", pageSize)
.queryParam("option", option)
.queryParam("keyword", keyword)
.build().toString();
}
public Integer getPageSize() {
return pageSize;
}
public void setPageSize(Integer pageSize) {
this.pageSize = pageSize;
}
public Integer getPage() {
return page;
}
public void setPage(Integer page) {
this.page = page;
}
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;
}
public Integer getOffset() {
return (page-1)*pageSize;
}
@Override
public String toString() {
return "SearchCondition{" +
"page=" + page +
", pageSize=" + pageSize +
"page=" + getOffset() +
", keyword='" + keyword + '\'' +
", option='" + option + '\'' +
'}';
}
}
<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>
@Override
public List<BoardDto> searchSelectPage(SearchCondition sc) throws Exception {
return session.selectList(namespace+"searchSelectPage", sc);
} // List<E> selectList(String statement, Object parameter)
@Override
public int searchResultCnt(SearchCondition sc) throws Exception {
return session.selectOne(namespace+"searchResultCnt", sc);
} // T selectOne(String statement, Object parameter)
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = {"file:src/main/webapp/WEB-INF/spring/root-context.xml"})
public class BoardDaoImplTest {
@Autowired
private BoardDao boardDao;
@Test
public void searchResultCntTest() throws Exception {
boardDao.deleteAll();
for (int i = 1; i<=20; i++){
BoardDto boardDto = new BoardDto("title"+i, "hello world", "asdf"+i);
boardDao.insert(boardDto);
}
SearchCondition sc = new SearchCondition(1, 10, "title2", "T"); // title2%
int cnt = boardDao.searchResultCnt(sc);
assertTrue(cnt==2); // 1~20 | title2, title20
sc = new SearchCondition(1, 10, "asdf2", "W"); // asdf2%
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, "hello world", "asdf"+i);
boardDao.insert(boardDto);
}
SearchCondition sc = new SearchCondition(1, 10, "title2", "T"); // asdf2%
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"); // title2%
list = boardDao.searchSelectPage(sc);
assertTrue(list.size()==2); // 1~20 | asdf2, asdf20
}
}
@Controller
@RequestMapping("/board")
public class BoardController {
@Autowired
BoardService boardService;
@PostMapping("/modify")
public String modify(Integer page, Integer pageSize,BoardDto boardDto, Model m,
HttpSession session, RedirectAttributes rattr){
String writer = (String)session.getAttribute("id");
boardDto.setWriter(writer);
try {
int rowCnt = boardService.modify(boardDto);
if(rowCnt!=1)
throw new Exception("Modify failed");
rattr.addAttribute("page", page);
rattr.addAttribute("pageSize", pageSize);
rattr.addFlashAttribute("msg","MOD_OK");
return "redirect:/board/list";
} catch (Exception e) {
e.printStackTrace();
m.addAttribute("mode", "modify"); // 수정 모드로
m.addAttribute(boardDto); // "boardDto" 생략
m.addAttribute("page", page);
m.addAttribute("pageSize", pageSize);
m.addAttribute("msg","MOD_ERR");
return "board"; // 등록하려던 내용을 보여줘야 함.
}
}
@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);
Instant startOfToday = LocalDate.now().atStartOfDay(ZoneId.systemDefault()).toInstant();
m.addAttribute("startOfToday", startOfToday.toEpochMilli());
} catch (Exception e) {
e.printStackTrace();
m.addAttribute("msg", "LIST_ERR");
m.addAttribute("totalCnt", 0);
}
return "boardList"; // 로그인을 한 상태이면, 게시판 화면으로 이동
}
}
@Repository
public class BoardServiceImpl implements BoardService {
@Autowired
BoardDao boardDao;
@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);
}
}
public interface BoardService {
List<BoardDto> getsearchResultPage(SearchCondition sc) throws Exception;
int getsearchResultCnt(SearchCondition sc) throws Exception;
}
public class PageHandler {
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){
this.totalPage = totalCnt / sc.getPageSize() + (totalCnt % sc.getPageSize()==0? 0:1);
this.sc.setPage(Math.min(sc.getPage(), totalPage)); // page가 totalPage보다 크지 않게
this.beginPage = (this.sc.getPage() -1) / NAV_SIZE * NAV_SIZE + 1; // 11 -> 11, 10 -> 1, 15->11. 따로 떼어내서 테스트
this.endPage = Math.min(beginPage + NAV_SIZE - 1, totalPage);
this.showPrev = beginPage!=1;
this.showNext = endPage!=totalPage;
}
public String getQueryString() {
return getQueryString(this.sc.getPage());
}
public String getQueryString(Integer page) {
// ?page=10&pageSize=10&option=A&keyword=title
return UriComponentsBuilder.newInstance()
.queryParam("page", page)
.queryParam("pageSize", sc.getPageSize())
.queryParam("option", sc.getOption())
.queryParam("keyword", sc.getKeyword())
.build().toString();
}
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]" : " ");
}
// toString() 생략
// getter & setter 생략
}
<%@ page language="java" contentType="text/html; charset=UTF-8"%>
<%@taglib prefix="c" uri="http://java.sun.com/jstl/core_rt" %>
<%@ page session="true"%>
<c:set var="loginId" value="${sessionScope.id}"/>
<c:set var="loginOutLink" value="${loginId=='' ? '/login/login' : '/login/logout'}"/>
<c:set var="loginOut" value="${loginId=='' ? 'Login' : loginId}"/>
<!DOCTYPE html>
<html lang="en">
<head>
<meta http-equiv="x-ua-compatible" content="IE-edge">
<meta name="viewport" content="width-device-width, initial-scale-1.0">
<meta charset="UTF-8">
<title>fastcampus</title>
<link rel="stylesheet" href="<c:url value='/css/menu.css'/>">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css">
<script src="https://code.jquery.com/jquery-1.11.3.js"></script>
<style>생략</style>
</head>
<body>
<div id="menu">
<ul>
<li id="logo">fastcampus</li>
<li><a href="<c:url value='/'/>">Home</a></li>
<li><a href="<c:url value='/board/list'/>">Board</a></li>
<li><a href="<c:url value='${loginOutLink}'/>">${loginOut}</a></li>
<li><a href="<c:url value='/register/add'/>">Sign in</a></li>
<li><a href=""><i class="fas fa-search small"></i></a></li>
</ul>
</div>
<script>
let msg = "${msg}";
if(msg=="WRT_ERR") alert("게시물 등록에 실패했습니다. 다시 시도해주세요.");
if(msg=="MOD_ERR") alert("게시물 등록에 실패했습니다. 다시 시도해주세요.");
</script>
<div class="container">
<%-- 게시물 읽기(mode="")/게시물 수정(mode="modify")/게시물 글쓰기(mode="new") --%>
<h2 class="writing-header">게시물 ${mode!="new" ? "읽기":(mode=="modify")? "수정":"글쓰기"}</h2>
<form action="" id="form" class="frm" method="post">
<%-- 읽어올 때는 readonly 수정할 때는 X --%>
<input type="hidden" name="bno" value="${boardDto.bno}">
<input type="text" name="title" value="<c:out value='${boardDto.title}'/>" placeholder="제목을 입력해 주세요." ${mode=="new" ?'':'readonly="readonly"'}>
<textarea name="content" rows="20" placeholder=" 내용을 입력해 주세요." ${mode=="new" ?'':'readonly="readonly"'}><c:out value="${boardDto.content}"/></textarea>
<c:if test="${mode eq 'new'}">
<button type="button" id="writeBtn" class="btn btn-write"><i class="fa fa-pencil"></i> 등록</button>
</c:if>
<c:if test="${mode ne 'new'}">
<button type="button" id="writeNewBtn" class="btn btn-write"><i class="fa fa-pencil"></i> 글쓰기</button>
</c:if>
<c:if test="${boardDto.writer eq loginId}">
<button type="button" id="modifyBtn" class="btn btn-modify"><i class="fa fa-edit"></i> 수정</button>
<button type="button" id="removeBtn" class="btn btn-remove"><i class="fa fa-trash"></i> 삭제</button>
</c:if>
<button type="button" id="listBtn" class="btn btn-list"><i class="fa fa-bars"></i> 목록</button>
</form>
</div>
<script>
$(document).ready(function(){
let formCheck = function() {
let form = document.getElementById("form");
// 내용이 비워있으면
if(form.title.value=="") {
alert("제목을 입력해 주세요.");
form.title.focus();
return false;
}
// 제목이 비워있으면
if(form.content.value=="") {
alert("내용을 입력해 주세요.");
form.content.focus();
return false;
}
return true;
}
$('#listBtn').on("click", function(){
// 브라우저 주소창에 다음 주소 입력 GET
location.href="<c:url value='/board/list${searchCondition.queryString}'/>";
});
$('#modifyBtn').on("click", function(){
// 1. 읽기 상태이면 수정 상태로 변경
let form = $('#form');
let isReadOnly = $("input[name=title]").attr('readonly');
if(isReadOnly=='readonly'){
$("input[name=title]").attr('readonly',false); // title
$("textarea").attr('readonly',false); // content
$("#modifyBtn").html("<i class='fa fa-pencil'></i> 등록");
$(".writing-header").html("게시글 수정");
return;
}
// 2. 수정 상태이면 수정된 내용을 서버로 전송하고 원래 게시물 목록 페이지로
form.attr("action", "<c:url value='/board/modify${searchCondition.queryString}'/>");
form.attr("method", "post");
if(formCheck())
form.submit();
});
$('#writeNewBtn').on("click", function(){
location.href="<c:url value='/board/write'/>";
});
$('#writeBtn').on("click", function(){
if(!confirm("정말로 등록하시겠습니까?")) return;
let form = $('#form');
form.attr("action", "<c:url value='/board/write'/>");
form.attr("method", "post");
if(formCheck())
form.submit();
});
$('#removeBtn').on("click", function(){
if(!confirm("정말로 삭제하시겠습니까?")) return;
let form = $("#form");
form.attr("action", "<c:url value='/board/remove${searchCondition.queryString}'/>");
form.attr("method", "post");
form.submit();
});
});
</script>
</body>
</html>
<%@ page language="java" contentType="text/html; charset=UTF-8"%>
<%@taglib prefix="c" uri="http://java.sun.com/jstl/core_rt" %>
<%@taglib prefix="fmt" uri="http://java.sun.com/jstl/fmt_rt" %>
<%@ page session="true"%>
<c:set var="loginId" value="${sessionScope.id}"/>
<c:set var="loginOutLink" value="${loginId=='' ? '/login/login' : '/login/logout'}"/>
<c:set var="loginOut" value="${loginId=='' ? 'Login' : loginId}"/>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>fastcampus</title>
<link rel="stylesheet" href="<c:url value='/css/menu.css'/>">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css">
<script src="https://code.jquery.com/jquery-1.11.3.js"></script>
<style>생략</style>
</head>
<body>
<div id="menu">
<ul>
<li id="logo">fastcampus</li>
<li><a href="<c:url value='/'/>">Home</a></li>
<li><a href="<c:url value='/board/list'/>">Board</a></li>
<li><a href="<c:url value='${loginOutLink}'/>">${loginOut}</a></li>
<li><a href="<c:url value='/register/add'/>">Sign in</a></li>
<li><a href=""><i class="fas fa-search small"></i></a></li>
</ul>
</div>
<script>
let msg = "${msg}";
if(msg=="LIST_ERR") alert("게시물 목록을 가져오는데 실패했습니다. 다시 시도해 주세요.");
if(msg=="READ_ERR") alert("삭제되었거나 없는 게시물입니다.");
if(msg=="DEL_ERR") alert("삭제되었거나 없는 게시물입니다.");
if(msg=="DEL_OK") alert("성공적으로 삭제되었습니다.");
if(msg=="WRT_OK") alert("성공적으로 등록되었습니다.");
if(msg=="MOD_OK") alert("성공적으로 수정되었습니다.");
</script>
<div style="text-align:center">
<%-- onclick function(){location.href=}을 생략하고 그 안의 location.href=만 써줌
location.href 주소 지정, GET방식 --%>
<div class="board-container">
<div class="search-container">
<form action="<c:url value="/board/list"/>" class="search-form" method="get">
<select class="search-option" name="option">
<option value="A" ${ph.sc.option=='A' || ph.sc.option=='' ? "selected" : ""}>제목+내용</option>
<option value="T" ${ph.sc.option=='T' ? "selected" : ""}>제목만</option>
<option value="W" ${ph.sc.option=='W' ? "selected" : ""}>작성자</option>
</select>
<input type="text" name="keyword" class="search-input"
type="text" value="${ph.sc.keyword}" placeholder="검색어를 입력해주세요">
<input type="submit" class="search-button" value="검색">
</form>
<button id="writeBtn" class="btn-write"token tag"><c:url value="/board/write"/>'"><i class="fa fa-pencil"></i> 글쓰기</button>
</div>
<table border="1">
<tr>
<th class="no">번호</th>
<th class="title">제목</th>
<th class="writer">이름</th>
<th class="reg_date">등록일</th>
<th class="view_cnt">조회수</th>
</tr>
<c:forEach var="boardDto" items="${list}">
<tr>
<td class="no">${boardDto.bno}</td>
<td class="title"><a href="<c:url value="/board/read${ph.sc.queryString}&bno=${boardDto.bno}"/>"><c:out value="${boardDto.title}"/></a></td>
<td class="writer">${boardDto.writer}</td>
<c:choose> <%-- 당일이면 시간, 그 전이면 날짜만--%>
<c:when test="${boardDto.reg_date.time >= startOfToday}">
<td class="regdate"><fmt:formatDate value="${boardDto.reg_date}" pattern="HH:mm" type="time"/></td>
</c:when>
<c:otherwise>
<td class="regdate"><fmt:formatDate value="${boardDto.reg_date}" pattern="yyyy-MM-dd" type="date"/></td>
</c:otherwise>
</c:choose>
<td class="viewcnt">${boardDto.view_cnt}</td>
</tr>
</c:forEach>
</table>
<br>
<div class="paging-container">
<div class="paging">
<c:if test="${totalCnt==null || totalCnt==0}">
<div> 게시물이 없습니다. </div>
</c:if>
<c:if test="${totalCnt!=null && totalCnt!=0}">
<c:if test="${ph.showPrev}">
<a class="page" href="<c:url value="/board/list${ph.sc.getQueryString(ph.beginPage-1)}"/>"><</a>
</c:if>
<c:forEach var="i" begin="${ph.beginPage}" end="${ph.endPage}">
<a class="page ${i==ph.sc.page? "paging-active" : ""}" href="<c:url value="/board/list${ph.sc.getQueryString(i)}"/>">${i}</a>
</c:forEach>
<c:if test="${ph.showNext}">
<a class="page" href="<c:url value="/board/list${ph.sc.getQueryString(ph.endPage+1)}"/>">></a>
</c:if>
</c:if>
</div>
</div>
</div>
</body>
</html>
참고) 자바의 정석 | 남궁성과 끝까지 간다