Spring legacy 프로젝트 게시판 만들기(6) - 검색
Spring legacy 프로젝트 게시판 만들기(5) - 게시판 페이징 view
Spring legacy 프로젝트 게시판 만들기(5) - 게시판 페이징 Spring legacy 프로젝트 게시판 만들기(4) - 수정하기 Spring legacy 프로젝트 게시판 만들기(3) - 상세조회 이전글 Spring legacy 프로젝트 게시판 만..
cronex.tistory.com
이번엔 list 중에서 검색하는 기능을 추가하도록 하겠습니다.
먼저 jsp를 구성하도록 하겠습니다. 페이징 div 아래 검색 div를 추가하였습니다.
- board.jsp(검색 form 추가)
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
<!-- taglib 추가 -->
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>페이징 게시판</title>
<style type="text/css">
.table>tbody tr:hover {
background: rgb(200, 200, 200);
cursor: pointer;
}
.table tr {
border-bottom: 1px solid #999;
}
.table {
padding: 30px;
margin: 0px auto;
font-size: 20px;
font-weight: normal;
}
.table thead {
border-top: 2px solid #666;
}
</style>
</head>
<body>
<jsp:include page="../common/sidebar.jsp" />
<div class="content">
<div style="padding-top: 100px;">
<table class="table"
style="border-collapse: collapse; width: 1200px; text-align: center;">
<colgroup>
<col width="15%">
<col width="20%">
<col width="40%">
<col width="15%">
</colgroup>
<thead>
<tr>
<th>번호</th>
<th>제목</th>
<th>작성자</th>
<th>작성일</th>
<tr>
</thead>
<tbody class="board-tbody">
<!-- forEach로 변경된 body부분 -->
<c:forEach items="${requestScope.list}" var="board">
<tr>
<td>${board.board_id}</td>
<td>${board.board_title}</td>
<td>${board.board_writer}</td>
<td>${board.board_date}</td>
</tr>
</c:forEach>
</tbody>
</table>
<!-- 글쓰기 버튼 -->
<div style="text-align: right; padding-right: 10%; margin-top: 20px;">
<button id="write">글쓰기</button>
</div>
<!-- 페이징 -->
<div style="text-align: center; margin-top: 30px;">
<c:choose>
<c:when test="${empty requestScope.searchValue}"> <!-- 1 -->
<button id="startPage"><<</button> <!-- 2 -->
<c:if test="${requestScope.pageInfo.pageNo <= 1}"> <!-- 3 -->
<button disabled><</button>
</c:if>
<c:if test="${requestScope.pageInfo.pageNo > 1}"> <!-- 4 -->
<button id="prevPage"><</button>
</c:if>
<c:forEach var="p" begin="${requestScope.pageInfo.startPage}"
end="${requestScope.pageInfo.endPage}" step="1"> <!-- 5 -->
<c:if test="${requestScope.pageInfo.pageNo eq p}"> <!-- 6 -->
<button disabled>
<c:out value="${p}"></c:out>
</button>
</c:if>
<c:if test="${requestScope.pageInfo.pageNo ne p}">
<!-- 7 -->
<button class="paging-button">
<c:out value="${p}"></c:out>
</button>
</c:if>
</c:forEach>
<c:if test="${requestScope.pageInfo.pageNo >= requestScope.pageInfo.maxPage}"> <!-- 8 -->
<button disabled>></button>
</c:if>
<c:if test="${requestScope.pageInfo.pageNo < requestScope.pageInfo.maxPage}"> <!-- 9 -->
<button id="nextPage">></button>
</c:if>
<button id="maxPage">>></button>
<!-- 10 -->
</c:when>
<c:otherwise> <!-- 검색시 페이징 -->
</c:otherwise>
</c:choose>
</div>
<!-- 검색 폼 -->
<form id="searchForm">
<div class="search-area" align="center">
<select id="searchCondition" name="type">
<option value="writer">작성자</option>
<option value="title">제목</option>
<option value="text">내용</option>
</select> <input type="search" id="searchValue" name="keyword">
<button type="button" id="search">검색하기</button>
</div>
</form>
</div>
</div>
<script type="text/javascript">
const PAGING_PATH = "${pageContext.servletContext.contextPath}/board";
$(function(){
<!-- detail 페이지 이동 js -->
/*list에서 게시물 클릭 시 해당 게시물 상세 페이지로 이동*/
$(".board-tbody > tr").on("click", function(){
let number = $(this).children().eq(0).text(); /*board number*/
location.href = "${pageContext.servletContext.contextPath}/board/" + number;
})
// << 버튼 처음 페이지로 이동
$("#startPage").on("click", function(){
location.href = PAGING_PATH + "?currentPage=1";
})
// < 버튼 이전 페이지로 이동
$("#prevPage").on("click", function(){
location.href = PAGING_PATH + "?currentPage=${reqeustScope.pageInfo.pageNo - 1}";
})
// > 이 후 페이지로 이동
$("#nextPage").on("click", function(){
location.href = PAGING_PATH + "?currentPage=${requestScope.pageInfo.pageNo + 1}";
})
// >> 끝 페이지로 이동
$("#maxPage").on("click", function(){
location.href = PAGING_PATH + "?currentPage=${requestScope.pageInfo.maxPage}";
})
// 페이징 버튼 클릭(1,2,3,4,5...) 해당 페이지로 이동
$(".paging-button").on("click", function(){
let pageNumber = $(this).text();
location.href = PAGING_PATH +"?currentPage="+pageNumber;
})
//작성 버튼 클릭 시 페이지 이동
$("#write").on("click", function(){
location.href = "${pageContext.servletContext.contextPath}/write";
})
})
</script>
</body>
</html>
브라우저에서 확인해 보면 아래와 같습니다.
검색하기 버튼을 누를 때 동작 할 함수를 작성하도록 하겠습니다.
-board.jsp
// 검색 버튼 클릭 시
$("#search").on("click", function(){
let $type = $("#searchCondition").children(":selected");
let $keyword = $("#searchValue").val();
if($keyword == "") {
alert("검색어를 입력해 주세요")
}else{
$("#searchForm").attr("action", PAGING_PATH);
$("#searchForm").attr("method", "get");
$("#searchForm").submit();
}
})
기존에 list요청을 받는 controller를 수정하여 새로 요청을 받을 controller를 작성하도록 하겠습니다.
-BoardController.java
package com.js.board.controller;
import java.util.List;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import com.js.board.model.dto.BoardDTO;
import com.js.board.model.dto.PageInfoDTO;
import com.js.board.model.dto.SearchDTO;
import com.js.board.model.service.BoardService;
import com.js.board.paging.Pagenation;
@Controller
public class BoardController {
private static final Logger log = LoggerFactory.getLogger(BoardController.class);
private final BoardService boardService;
@Autowired
public BoardController(BoardService boardService) {
this.boardService = boardService;
}
/*
*board 리스트 페이지 요청 controller
*@param model Model 객체
*@param currentPage 페이징 번호
*@return viewResolver에 전달할 view path String
*/
@RequestMapping(value ="/board", method = RequestMethod.GET)
public String board(Model model,
@RequestParam(value="currentPage", defaultValue = "1", required = false) String currentPage,
@ModelAttribute SearchDTO search) {
/*기본 페이징 값 1로 설정*/
int pageNo = 1;
int page;
try {
page = Integer.parseInt(currentPage);
}catch(NumberFormatException e){
page = 1;
}
/*넘어온 currentPage 값이 0이거나 0보다 작다면 기본적으로 1로 설정*/
if(page <= 0) {
pageNo = 1;
}else {
pageNo = page;
}
/*db에 저장되어 있는 게시판 갯수 조회*/
int totalCount = boardService.totalCount(search);
/*페이징 계산 후 pageInfo에 값을 담는다.*/
search = (SearchDTO)Pagenation.getPageInfo(pageNo, totalCount, search);
/*starRow, endRow에 해당하는 list 조회*/
List<BoardDTO> list = boardService.selectBoardList(search);
/*model에 담기*/
model.addAttribute("list", list);
model.addAttribute("pageInfo", search);
log.info("list : {}", list);
log.info("pageInfo : {}", search);
return "board/board";
}
}
기존에서 변경된게 있다면
@ModelAttribute SearchDTO seach | PageInfo class를 확장한 객체 type과 keyword를 필드로 갖고있다. |
int totalCount | 기존에는 매개변수 없이 전체 리스트 갯수를 조회해왔지만 검색어를 통한 리스트 갯수를 조회하기 위해 keyword와 type으로 조회한다. |
Pagenation.getPageInfo(pageNo, totalCount, search) | 페이징을 계산하여 반환해주는 메소드로 search를 매개변수로 추가로 전달하도록 변경하였습니다. |
boardService.selectBoardList(search) | 기존에는 list를 1~ 10, 11 ~ 20 ... ROW를 기준으로 조회해 왔다면 검색어를 추가적으로 조회하여 1 ~ 10, 11 ~20...ROW를 기준으로 조회한다. |
추가된 SearchDTO를 살펴 보도록 하겠습니다.
- SearchDTO.java
package com.js.board.model.dto;
public class SearchDTO extends PageInfoDTO implements java.io.Serializable{
private String type; /*검색 타입*/
private String keyword; /*검색어*/
public SearchDTO() {}
public SearchDTO(String type, String keyword) {
this.type = type;
this.keyword = keyword;
}
public SearchDTO(int pageNo, int totalCount, int limit, int buttonAmount, int maxPage, int startPage, int endPage,
int startRow, int endRow) {
super(pageNo, totalCount, limit, buttonAmount, maxPage, startPage, endPage, startRow, endRow);
}
public String getType() {
return type;
}
public void setType(String type) {
this.type = type;
}
public String getKeyword() {
return keyword;
}
public void setKeyword(String keyword) {
this.keyword = keyword;
}
@Override
public String toString() {
return "SearchDTO [type=" + type + ", keyword=" + keyword + ", toString()=" + super.toString() + "]";
}
}
SearchDTO | PageInfo 를 확장한 class 검색어 및 검색 타입을 담을 수 있도록 필드를 추가 하였습니다. |
기존에 사용하던 PageInfo를 상속받아 SearchDTO.class를 만들었습니다. 페이징 및 정보를 담기 위해 만들었습니다.
다음으로 수정된 페이징 계산 class 및 메소드를 보도록 하겠습니다.
- Pagenation.java(SearchDTO 매개변수 추가)
package com.js.board.paging;
import com.js.board.model.dto.PageInfoDTO;
import com.js.board.model.dto.SearchDTO;
public class Pagenation {
public static PageInfoDTO getPageInfo(int pageNo, int totalCount, PageInfoDTO search) {
int maxPage;
int startPage;
int endPage;
int startRow;
int endRow;
int limit = 10;
int buttonAmount = 10;
/*총 페이지 수 계산
* ex) 123개의 게시물 한 페이지당 10개씩 보여지는 경우
* 짜투리목록이 1페이지 추가로 필요
* 1페이지 추가 하기 위해 + 0.9
* 뒤에 더하기는 가중치 만큼 더하는 것 10개씩일 때는 하나의 게시물의 가중치는 10% 이니까 0.9
* 5개 일때는 게시물 한 개의 가중치는 20% 이니까 0.8
* 20개 일때는 게시물 한 개의 가중치는 5% 이니까 0.95
* */
// maxPage = (int) (((double) totalCount / limit) + 0.9);
maxPage = (int) Math.ceil((double) totalCount / limit); //올림처리
/*현재 페이지에 보여줄 시작페이지 수
* 10개씩 보여지게 할 경우
* 1, 11, 21, 31...
* 5개 씩일 경우
* 1, 6, 11, 16,...
* 뒤에 덧셈은 버튼 가중치에 대한 덧셈
* 앞자리 구해서 + 1
* 배수의 + 1
* */
// startPage = (((int) ((double) pageNo / buttonAmount + 0.9)) - 1) * buttonAmount + 1;
startPage = (int)(Math.ceil((double) pageNo / buttonAmount) - 1) * buttonAmount + 1;
/*
* 목록 아래 쪽에 보여질 마지막 페이지 수
* */
endPage = startPage + buttonAmount - 1;
/*maxPage가 더 작은 경우 마지막 페이지가 maxPage가 된다.*/
if(endPage > maxPage) {
endPage = maxPage;
}
/*마지막 페이지는 0이 될수 없기 때문에 게시물이 아무것도 존재하지 않으면 max 페이지와 endPage를 1로 바꿔준다.*/
if(maxPage == 0 && endPage == 0) {
maxPage = startPage;
endPage = startPage;
}
/*조회를 시작할 행의 번호 수와 마지막 행 번호를 계산한다.*/
startRow = (pageNo - 1) * limit + 1;
endRow = startRow + limit - 1;
search.setPageNo(pageNo);
search.setTotalCount(totalCount);
search.setLimit(limit);
search.setButtonAmount(buttonAmount);
search.setMaxPage(maxPage);
search.setStartPage(startPage);
search.setEndPage(endPage);
search.setStartRow(startRow);
search.setEndRow(endRow);
return search;
}
}
달라진 점은 검색어 및 검색타입 정보를 갖고 있는 search 객체를 넘겨 받아 페이징 정보를 setter를 이용하여 설정 후 search를 return 하도록 코드를 수정하였습니다.
-BoardService.java(interface)
package com.js.board.model.service;
import java.util.List;
import com.js.board.model.dto.BoardDTO;
import com.js.board.model.dto.PageInfoDTO;
import com.js.board.model.dto.SearchDTO;
public interface BoardService {
/*select board list*/
public List<BoardDTO> selectBoardList(PageInfoDTO search);
/*select board*/
public BoardDTO selectById(int number);
/*update board*/
public void updateById(BoardDTO board);
/*count board*/
public int totalCount(SearchDTO search);
/*insert board*/
public int insertBoard(BoardDTO board);
}
selectBoardList 메소드에 PageInfoDTO 매개변수가 추가되었습니다. |
- BoardServiceImpl.java
package com.js.board.model.service;
import java.util.List;
import org.mybatis.spring.SqlSessionTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import com.js.board.model.dto.BoardDTO;
import com.js.board.model.dto.PageInfoDTO;
import com.js.board.model.dto.SearchDTO;
import com.js.board.model.repository.BoardRepository;
@Service("boardService")
public class BoardServiceImpl implements BoardService{
private final BoardRepository boardRepository;
private final SqlSessionTemplate sqlSession;
@Autowired
public BoardServiceImpl(BoardRepository boardRepository, SqlSessionTemplate sqlSession) {
this.boardRepository = boardRepository;
this.sqlSession = sqlSession;
}
/*board list를 조회할 때 사용하는 service method
* @param pageInfo 페이징 객체
* @return board List 정보
* */
@Override
public List<BoardDTO> selectBoardList(PageInfoDTO search) {
return boardRepository.selectBoardList(sqlSession, search);
}
/*
* board를 조회할 때 사용하는 service method
* @param number 게시판 번호
* @return board 정보
* */
@Override
public BoardDTO selectById(int number) {
return boardRepository.selectById(number, sqlSession);
}
/*
* board를 수정할 때 사용하는 service method
* @param board 게시물 수정정보가 담긴 객체
* @return
* */
@Override
public void updateById(BoardDTO board) {
boardRepository.updateById(board, sqlSession);
}
/*
*count board list method
*@param search 검색정보가 담겨있는 객체
*@return count board list
* */
@Override
public int totalCount(SearchDTO search) {
return boardRepository.totalCount(sqlSession, search);
}
/*
* board insert method
* @param board 입력한 게시물 정보
* return boardNumber
* */
@Override
public int insertBoard(BoardDTO board) {
int boardNumber = boardRepository.selectNextVal(sqlSession);
board.setBoard_id(boardNumber);
boardRepository.insertBoard(sqlSession, board);
return boardNumber;
}
}
- BoardRepository.java (interface)
package com.js.board.model.repository;
import java.util.List;
import org.mybatis.spring.SqlSessionTemplate;
import com.js.board.model.dto.BoardDTO;
import com.js.board.model.dto.PageInfoDTO;
import com.js.board.model.dto.SearchDTO;
public interface BoardRepository {
/*select Board List*/
public List<BoardDTO> selectBoardList(SqlSessionTemplate sqlSession, PageInfoDTO search);
/*select Board*/
public BoardDTO selectById(int number, SqlSessionTemplate sqlSession);
/*update Board*/
public int updateById(BoardDTO board, SqlSessionTemplate sqlSession);
/*count board*/
public int totalCount(SqlSessionTemplate sqlSession, SearchDTO search);
/*select board nextVal*/
public int selectNextVal(SqlSessionTemplate sqlSession);
/*insert board*/
public void insertBoard(SqlSessionTemplate sqlSession, BoardDTO board);
/*search count*/
public int keywordCount(SqlSessionTemplate sqlSession, SearchDTO search);
}
selectBoardList() 메소드에 PageInfoDTO 매개변수가 추가되었습니다. |
- BoardRepositoryImpl.java
package com.js.board.model.repository;
import java.util.List;
import org.mybatis.spring.SqlSessionTemplate;
import org.springframework.stereotype.Repository;
import com.js.board.model.dto.BoardDTO;
import com.js.board.model.dto.PageInfoDTO;
import com.js.board.model.dto.SearchDTO;
@Repository("boardRepository")
public class BoardRepositoryImpl implements BoardRepository{
/*
* board List 조회 Repository method
* @param sqlSession service에서 전달받은 sqlSessionTemplate 쿼리문 실행할 객체
* @param pageInfo 페이징 객체
* @return db에서 조회한 board List정보
* */
@Override
public List<BoardDTO> selectBoardList(SqlSessionTemplate sqlSession, PageInfoDTO search) {
return sqlSession.selectList("board.selectList", search);
}
/*
* board 조회 Repository method
* @param number board 번호
* @param sqlSession service에서 전달받은 sqlSessionTemplate 쿼리문 실행할 객체
* @return db에서 조회한 board 정보
* */
@Override
public BoardDTO selectById(int number, SqlSessionTemplate sqlSession) {
return sqlSession.selectOne("board.selectById", number);
}
/*
* board update Repository method
* @param board 수정할 보드정보 객체
* @param sqlSession 쿼리문 실행할 객체
* */
@Override
public int updateById(BoardDTO board, SqlSessionTemplate sqlSession) {
return sqlSession.update("board.updateById", board);
}
/*count board list Repository
* @param sqlSession 쿼리문 실행할 객체
* @return 보드리스트 갯수
* */
@Override
public int totalCount(SqlSessionTemplate sqlSession, SearchDTO search) {
return sqlSession.selectOne("board.totalCount", search);
}
/*
* board sequence nextval 조회
* param sqlSession 쿼리문 실행할 객체
* return 시퀀스 nextval 값
* */
@Override
public int selectNextVal(SqlSessionTemplate sqlSession) {
return sqlSession.selectOne("board.boardNextval");
}
/*
* board 정보 db에 입력(insert)
* @param sqlSession 쿼리문 실행할 객체
* @param board 입력한 board정보
* */
@Override
public void insertBoard(SqlSessionTemplate sqlSession, BoardDTO board) {
sqlSession.insert("board.insertBoard", board);
}
/*
* 조건 검색 게시물 count
* @param sqlSession 쿼리문 실행할 객체
* @param search 검색 정보가 담겨있는 객체
* */
@Override
public int keywordCount(SqlSessionTemplate sqlSession, SearchDTO search) {
return sqlSession.selectOne("board.keywordCount", search);
}
}
mapper도 동적쿼리를 이용하여 수정하였습니다. 변경된 쿼리문을 살펴 보도록 하겠습니다.
-board-mapper.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="board">
<select id="selectList" resultType="com.js.board.model.dto.BoardDTO" parameterType="com.js.board.model.dto.SearchDTO">
SELECT
A.RNUM
, A.BOARD_ID
, A.BOARD_CATEGORY
, A.BOARD_WRITER
, A.BOARD_TITLE
, A.BOARD_TEXT
, A.BOARD_DATE
, A.BOARD_REPLY
, A.BOARD_LEVEL
, A.BOARD_STATUS
FROM(SELECT ROWNUM AS RNUM
, B.BOARD_ID
, B.BOARD_CATEGORY
, B.BOARD_WRITER
, B.BOARD_TITLE
, B.BOARD_TEXT
, B.BOARD_DATE
, B.BOARD_REPLY
, B.BOARD_LEVEL
, B.BOARD_STATUS
FROM(SELECT C.BOARD_ID
, C.BOARD_CATEGORY
, C.BOARD_WRITER
, C.BOARD_TITLE
, C.BOARD_TEXT
, C.BOARD_DATE
, C.BOARD_REPLY
, C.BOARD_LEVEL
, C.BOARD_STATUS
FROM BOARD C
WHERE BOARD_LEVEL = 0
AND BOARD_STATUS = 'N'
<if test="(type != null and keyword != null) and (type != '' and keyword != '')">
<choose>
<when test="type == 'writer'">
AND INSTR(BOARD_WRITER, #{keyword}) > 0
</when>
<when test="type == 'title'">
AND INSTR(BOARD_TITLE, #{keyword}) > 0
</when>
<when test="type == 'text'">
AND INSTR(BOARD_TEXT, #{keyword}) > 0
</when>
<otherwise>
</otherwise>
</choose>
</if>
ORDER BY C.BOARD_ID DESC
) B
)A
WHERE A.RNUM BETWEEN #{startRow} AND #{endRow}
</select>
<select id="totalCount" parameterType="com.js.board.model.dto.SearchDTO" resultType="_int">
SELECT
COUNT(*)
FROM BOARD
WHERE BOARD_LEVEL = 0
AND BOARD_STATUS = 'N'
<if test="(type != null and keyword != null) and (type !='' and keyword != '') ">
<choose>
<when test="type == 'writer'">
AND INSTR(BOARD_WRITER, #{keyword}) > 0
</when>
<when test="type == 'title'">
AND INSTR(BOARD_TITLE, #{keyword}) > 0
</when>
<when test="type == 'text'">
AND INSTR(BOARD_TEXT, #{keyword}) > 0
</when>
<otherwise>
</otherwise>
</choose>
</if>
</select>
</mapper>
id = selectList | 기존 페이징을 이용하여 조회하던 쿼리문에서 추가적으로 조건을 이용해 검색어 및 검색 타입으로 조회 할 수 있도록 수정하였습니다. 조건으로는 keyword(검색어), type(검색종류)로 기존(전체) 페이징이라면 keyword와 type이 없으니까 null 조건을 주었고 검색어를 입력하지 않고 검색했을 경우를 대비해 빈문자열일 경우 조건문 안 쿼리는 실행되지않도록 하였습니다. |
id = totalCount | 위와 같은 조건을 추가하여 조건에 맞는 게시물 숫자를 조회해오도록 수정하였습니다. |
위에서 설정한 함수에서 볼 수 있듯이 입력을 하지않으면 요청을 보내지 않게 설정하였기 때문에 보통의 방법으로는 null 넘어오지는 않을 것 같습니다.
검색어를 입력하여 잘 조회해 오는지 보도록 하겠습니다.
type 으로는 제목을 검색어로는 "문자"를 입력한 후 검색하기 버튼을 클릭해 보겠습니다.
DEBUG: board.totalCount - ==> Preparing: SELECT COUNT(*) FROM BOARD WHERE BOARD_LEVEL = 0 AND BOARD_STATUS = 'N' AND INSTR(BOARD_TITLE, ?) > 0
DEBUG: board.totalCount - ==> Parameters: 문자(String)
INFO : jdbc.sqlonly - SELECT COUNT(*) FROM BOARD WHERE BOARD_LEVEL = 0 AND BOARD_STATUS = 'N' AND INSTR(BOARD_TITLE,
'문자') > 0
INFO : jdbc.resultsettable -
|---------|
|count(*) |
|---------|
|2 |
|---------|
DEBUG: org.mybatis.spring.SqlSessionUtils - Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@189a129b]
DEBUG: org.mybatis.spring.SqlSessionUtils - Creating a new SqlSession
DEBUG: org.mybatis.spring.SqlSessionUtils - SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@35a0234a] was not registered for synchronization because synchronization is not active
DEBUG: org.springframework.jdbc.datasource.DataSourceUtils - Fetching JDBC Connection from DataSource
DEBUG: org.mybatis.spring.transaction.SpringManagedTransaction - JDBC Connection [jdbc:oracle:thin:@localhost:1521:XE, UserName=BOARD, Oracle JDBC driver] will not be managed by Spring
DEBUG: board.selectList - ==> Preparing: SELECT A.RNUM , A.BOARD_ID , A.BOARD_CATEGORY , A.BOARD_WRITER , A.BOARD_TITLE , A.BOARD_TEXT , A.BOARD_DATE , A.BOARD_REPLY , A.BOARD_LEVEL , A.BOARD_STATUS FROM(SELECT ROWNUM AS RNUM , B.BOARD_ID , B.BOARD_CATEGORY , B.BOARD_WRITER , B.BOARD_TITLE , B.BOARD_TEXT , B.BOARD_DATE , B.BOARD_REPLY , B.BOARD_LEVEL , B.BOARD_STATUS FROM(SELECT C.BOARD_ID , C.BOARD_CATEGORY , C.BOARD_WRITER , C.BOARD_TITLE , C.BOARD_TEXT , C.BOARD_DATE , C.BOARD_REPLY , C.BOARD_LEVEL , C.BOARD_STATUS FROM BOARD C WHERE BOARD_LEVEL = 0 AND BOARD_STATUS = 'N' AND INSTR(BOARD_TITLE, ?) > 0 ORDER BY C.BOARD_ID DESC ) B )A WHERE A.RNUM BETWEEN ? AND ?
DEBUG: board.selectList - ==> Parameters: 문자(String), 1(Integer), 10(Integer)
INFO : jdbc.sqlonly - SELECT A.RNUM , A.BOARD_ID , A.BOARD_CATEGORY , A.BOARD_WRITER , A.BOARD_TITLE , A.BOARD_TEXT
, A.BOARD_DATE , A.BOARD_REPLY , A.BOARD_LEVEL , A.BOARD_STATUS FROM(SELECT ROWNUM AS RNUM
, B.BOARD_ID , B.BOARD_CATEGORY , B.BOARD_WRITER , B.BOARD_TITLE , B.BOARD_TEXT , B.BOARD_DATE
, B.BOARD_REPLY , B.BOARD_LEVEL , B.BOARD_STATUS FROM(SELECT C.BOARD_ID , C.BOARD_CATEGORY
, C.BOARD_WRITER , C.BOARD_TITLE , C.BOARD_TEXT , C.BOARD_DATE , C.BOARD_REPLY , C.BOARD_LEVEL
, C.BOARD_STATUS FROM BOARD C WHERE BOARD_LEVEL = 0 AND BOARD_STATUS = 'N' AND INSTR(BOARD_TITLE,
'문자') > 0 ORDER BY C.BOARD_ID DESC ) B )A WHERE A.RNUM BETWEEN 1 AND 10
INFO : jdbc.resultsettable -
|---------|---------|---------------|-------------|------------|-----------|-----------|------------|------------|-------------|
|rnum |board_id |board_category |board_writer |board_title |board_text |board_date |board_reply |board_level |board_status |
|---------|---------|---------------|-------------|------------|-----------|-----------|------------|------------|-------------|
|[unread] |159 |0 |sj |문자 |문자 |2021-06-21 |[null] |0 |N |
|[unread] |158 |0 |js |문자 |문자 |2021-06-21 |[null] |0 |N |
|---------|---------|---------------|-------------|------------|-----------|-----------|------------|------------|-------------|
DEBUG: board.selectList - <== Total: 2
잘 적용된 것을 확인할 수 있습니다.
다음으로는 페이지버튼을 클릭하게 되면 이전에 검색했던 검색어가 날아가 기존 전체 리스트를 조회하게 되어버립니다. 아래와 같이 말이죠
url을 보면 ?type=writer&keyword=sj 쿼리스트링을 통해 요청을 하였는데요, 여기서 2페이지를 클릭하게 되면
?currentPage=2 로 변경되면서 기존에 검색이 유지되지 않습니다.
그 이유는 현재 페이징을 구성하고 있는 코드들이 기존에 사용하던 전체 리스트 기준으로 되어있기 때문입니다.
페이징부분을 수정을 통해 검색을 유지해보도록 하겠습니다.
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
<!-- taglib 추가 -->
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>페이징 게시판</title>
<style type="text/css">
.table>tbody tr:hover {
background: rgb(200, 200, 200);
cursor: pointer;
}
.table tr {
border-bottom: 1px solid #999;
}
.table {
padding: 30px;
margin: 0px auto;
font-size: 20px;
font-weight: normal;
}
.table thead {
border-top: 2px solid #666;
}
</style>
</head>
<body>
<jsp:include page="../common/sidebar.jsp" />
<div class="content">
<div style="padding-top: 100px;">
<div style="margin-bottom : 13px; padding-left : 161px;">
<button type="button" id="all-button">전체글</button>
</div>
<table class="table"
style="border-collapse: collapse; width: 1200px; text-align: center;">
<colgroup>
<col width="15%">
<col width="20%">
<col width="40%">
<col width="15%">
</colgroup>
<thead>
<tr>
<th>번호</th>
<th>제목</th>
<th>작성자</th>
<th>작성일</th>
<tr>
</thead>
<tbody class="board-tbody">
<!-- forEach로 변경된 body부분 -->
<c:forEach items="${requestScope.list}" var="board">
<tr>
<td>${board.board_id}</td>
<td>${board.board_title}</td>
<td>${board.board_writer}</td>
<td>${board.board_date}</td>
</tr>
</c:forEach>
</tbody>
</table>
<!-- 글쓰기 버튼 -->
<div style="text-align: right; padding-right: 10%; margin-top: 20px;">
<button id="write">글쓰기</button>
</div>
<!-- 페이징 -->
<div style="text-align: center; margin-top: 30px;">
<c:choose>
<c:when test="${empty requestScope.pageInfo.keyword and
empty requestScope.pageInfo.type}"> <!-- 1 -->
<button id="startPage"><<</button> <!-- 2 -->
<c:if test="${requestScope.pageInfo.pageNo <= 1}"> <!-- 3 -->
<button disabled><</button>
</c:if>
<c:if test="${requestScope.pageInfo.pageNo > 1}"> <!-- 4 -->
<button id="prevPage"><</button>
</c:if>
<c:forEach var="p" begin="${requestScope.pageInfo.startPage}"
end="${requestScope.pageInfo.endPage}" step="1"> <!-- 5 -->
<c:if test="${requestScope.pageInfo.pageNo eq p}"> <!-- 6 -->
<button disabled>
<c:out value="${p}"></c:out>
</button>
</c:if>
<c:if test="${requestScope.pageInfo.pageNo ne p}">
<!-- 7 -->
<button class="paging-button">
<c:out value="${p}"></c:out>
</button>
</c:if>
</c:forEach>
<c:if test="${requestScope.pageInfo.pageNo >= requestScope.pageInfo.maxPage}"> <!-- 8 -->
<button disabled>></button>
</c:if>
<c:if test="${requestScope.pageInfo.pageNo < requestScope.pageInfo.maxPage}"> <!-- 9 -->
<button id="nextPage">></button>
</c:if>
<button id="maxPage">>></button>
<!-- 10 -->
</c:when>
<c:otherwise> <!-- 검색시 페이징 -->
<button id="searchStartPage"><<</button> <!-- 11 -->
<c:if test="${requestScope.pageInfo.pageNo <= 1}"> <!-- 12 -->
<button disabled><</button>
</c:if>
<c:if test="${requestScope.pageInfo.pageNo > 1}"> <!-- 13 -->
<button id="searchPrevPage"><</button>
</c:if>
<c:forEach var="p" begin="${requestScope.pageInfo.startPage}"
end="${requestScope.pageInfo.endPage}" step="1"> <!-- 14 -->
<c:if test="${requestScope.pageInfo.pageNo eq p}"> <!-- 15 -->
<button disabled>
<c:out value="${p}"></c:out>
</button>
</c:if>
<c:if test="${requestScope.pageInfo.pageNo ne p}"> <!-- 16 -->
<button class="search-paging-button">
<c:out value="${p}"></c:out>
</button>
</c:if>
</c:forEach>
<c:if
test="${requestScope.pageInfo.pageNo >= requestScope.pageInfo.maxPage}"> <!-- 17 -->
<button disabled>></button>
</c:if>
<c:if
test="${requestScope.pageInfo.pageNo < requestScope.pageInfo.maxPage}"> <!-- 18 -->
<button id="searchNextPage">></button>
</c:if>
<button id="searchMaxPage">>></button> <!-- 19 -->
</c:otherwise>
</c:choose>
</div>
<!-- 검색 폼 -->
<form id="searchForm">
<div class="search-area" align="center"> <!-- 20 -->
<select id="searchCondition" name="type">
<option value="writer"<c:if test="${requestScope.pageInfo.type eq 'writer' }">selected</c:if>>작성자</option>
<option value="title" <c:if test="${requestScope.pageInfo.type eq 'title' }">selected</c:if>>제목</option>
<option value="text" <c:if test="${requestScope.pageInfo.type eq 'text' }">selected</c:if>>내용</option>
</select> <input type="search" id="searchValue" name="keyword" value="${requestScope.pageInfo.keyword }">
<button type="button" id="search">검색하기</button>
</div>
</form>
</div>
</div>
<!-- detail 페이지 이동 js -->
<script type="text/javascript">
const PAGING_PATH = "${pageContext.servletContext.contextPath}/board";
$(function(){
/*list에서 게시물 클릭 시 해당 게시물 상세 페이지로 이동*/
$(".board-tbody > tr").on("click", function(){
let number = $(this).children().eq(0).text(); /*board number*/
location.href = "${pageContext.servletContext.contextPath}/board/" + number;
})
// << 버튼 처음 페이지로 이동
$("#startPage").on("click", function(){
location.href = PAGING_PATH + "?currentPage=1";
})
// < 버튼 이전 페이지로 이동
$("#prevPage").on("click", function(){
location.href = PAGING_PATH + "?currentPage=${reqeustScope.pageInfo.pageNo - 1}";
})
// > 이 후 페이지로 이동
$("#nextPage").on("click", function(){
location.href = PAGING_PATH + "?currentPage=${requestScope.pageInfo.pageNo + 1}";
})
// >> 끝 페이지로 이동
$("#maxPage").on("click", function(){
location.href = PAGING_PATH + "?currentPage=${requestScope.pageInfo.maxPage}";
})
// 페이징 버튼 클릭(1,2,3,4,5...) 해당 페이지로 이동
$(".paging-button").on("click", function(){
let pageNumber = $(this).text();
location.href = PAGING_PATH +"?currentPage="+pageNumber;
})
//작성 버튼 클릭 시 페이지 이동
$("#write").on("click", function(){
location.href = "${pageContext.servletContext.contextPath}/write";
})
// 검색 버튼 클릭 시
$("#search").on("click", function(){
let $type = $("#searchCondition").children(":selected");
let $keyword = $("#searchValue").val();
if($keyword == "") {
alert("검색어를 입력해 주세요")
}else{
$("#searchForm").attr("action", PAGING_PATH);
$("#searchForm").attr("method", "get");
$("#searchForm").submit();
}
})
</script>
</body>
</html>
1 | 조건을 이용하여 keyword(검색어) 와 type(검색 종류) 가 없다면 기존에 사용하던(전체 리스트 페이징)을 사용하도록 하였습니다. |
otherwise | 검색어를 통해 검색 리스트를 조회하였을 때 페이징 부분입니다. |
11 | "<<" 버튼 입니다. 검색어를 통한 리스트 목록에서 가장 최신 목록으로 이동합니다. |
12 | "<" 버튼 입니다. 현재 페이지(pageNo)가 1이거나 1보다 작다면 이전 페이지는 없기 때문에 "disabled" 처리 하였습니다. |
13 | "<" 버튼입니다. 현재 페이지(pageNo)가 1보다 크다면 이전 페이지가 존재 하기에 "abled" 처리 하였습니다. |
14 | forEach문을 사용하여 현재 보여지는 시작 페이지(startPage) 부터 (endPage)를 구성합니다. ex) [1 2 3 4 5], [11 12 13 14 15].... |
15 | 현재 페이지(pageNo) 가 forEach문을 돌고 있는 값(p)과 같다면 해당 버튼을 "disabled" 처리 합니다. |
16 | 현재 페이지(pageNo) 가 forEach문을 돌고 있는 값(p)과 다르다면 해당 버튼을 "abled"처리 합니다. |
17 | ">"버튼 입니다. 현재 페이지(pageNo) 가 나타낼 수 있는 전체 페이지(maxPage)값 보다 크거나 같다면 다음 페이지는 없기 때문에 "disabled"처리 하였습니다. |
18 | ">"버튼 입니다. 현재 페이지(pageNo)가 나타낼 수 있는 전체 페이지(maxPage)값 보다 작다면 이 후 페이지가 있기 때문에 "abeld" 처리 하였습니다. |
19 | ">>" 버튼 입니다. maxPage로 이동합니다. |
20 | 작성 form 태그 입니다. option 태그안에 속성으로 조건식을 설정하여 현재 type이 같은 option에 selected 속성을 부여하도록 설정하였습니다. 검색어 입력 input 태그에도 value로 해당 검색어 값을 갖도록 설정하였습니다. |
이어서 해당 버튼 클릭 시 동작할 함수를 작성 하도록 하겠습니다.
// 검색 페이징 << 버튼 처음 페이지로 이동
$("#searchStartPage").on("click", function(){
location.href = PAGING_PATH + "?currentPage=1&type=${requestScope.pageInfo.type}&keyword=${requestScope.pageInfo.keyword}";
})
// 검색 페이징 < 버튼 이전 페이지로 이동
$("#searchPrevPage").on("click", function(){
location.href = PAGING_PATH + "?currentPage=${requestScope.pageInfo.pageNo - 1}&type=${requestScope.pageInfo.type}&keyword=${requestScope.pageInfo.keyword}";
})
//검색 페이징 페이지(1,2,3,4,5) 클릭 시 해당 페이지 이동
$(".search-paging-button").on("click", function(){
let pageNumber = $(this).text();
location.href = PAGING_PATH + "?currentPage=" + pageNumber +"&type=${requestScope.pageInfo.type}&keyword=${requestScope.pageInfo.keyword}";
})
//검색 페이징 > 버튼 이 후 페이지로 이동
$("#searchNextPage").on("click", function(){
location.href= PAGING_PATH + "?currentPage=${requestScope.pageInfo.pageNo + 1}&type=${requestScope.pageInfo.type}&keyword=${requestScope.pageInfo.keyword}";
})
//검색 페이징 >> 버튼 끝 페이지로 이동
$("#searchMaxPage").on("click", function(){
location.href = PAGING_PATH + "?currentPage=${requestScope.pageInfo.maxPage}&type=${requestScope.pageInfo.type}&keyword=${requestScope.pageInfo.keyword}";
})
마지막으로 다시 전체 리스트를 요청 할 수 있도록 table 윗 부분에 전체글 버튼을 만들었습니다.
아래 board.jsp의 전체 코드입니다
-board.jsp
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
<!-- taglib 추가 -->
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>페이징 게시판</title>
<style type="text/css">
.table>tbody tr:hover {
background: rgb(200, 200, 200);
cursor: pointer;
}
.table tr {
border-bottom: 1px solid #999;
}
.table {
padding: 30px;
margin: 0px auto;
font-size: 20px;
font-weight: normal;
}
.table thead {
border-top: 2px solid #666;
}
</style>
</head>
<body>
<jsp:include page="../common/sidebar.jsp" />
<div class="content">
<div style="padding-top: 100px;">
<div style="margin-bottom : 13px; padding-left : 161px;">
<button type="button" id="all-button">전체글</button>
</div>
<table class="table"
style="border-collapse: collapse; width: 1200px; text-align: center;">
<colgroup>
<col width="15%">
<col width="20%">
<col width="40%">
<col width="15%">
</colgroup>
<thead>
<tr>
<th>번호</th>
<th>제목</th>
<th>작성자</th>
<th>작성일</th>
<tr>
</thead>
<tbody class="board-tbody">
<!-- forEach로 변경된 body부분 -->
<c:forEach items="${requestScope.list}" var="board">
<tr>
<td>${board.board_id}</td>
<td>${board.board_title}</td>
<td>${board.board_writer}</td>
<td>${board.board_date}</td>
</tr>
</c:forEach>
</tbody>
</table>
<!-- 글쓰기 버튼 -->
<div style="text-align: right; padding-right: 10%; margin-top: 20px;">
<button id="write">글쓰기</button>
</div>
<!-- 페이징 -->
<div style="text-align: center; margin-top: 30px;">
<c:choose>
<c:when test="${empty requestScope.pageInfo.keyword and
empty requestScope.pageInfo.type}"> <!-- 1 -->
<button id="startPage"><<</button> <!-- 2 -->
<c:if test="${requestScope.pageInfo.pageNo <= 1}"> <!-- 3 -->
<button disabled><</button>
</c:if>
<c:if test="${requestScope.pageInfo.pageNo > 1}"> <!-- 4 -->
<button id="prevPage"><</button>
</c:if>
<c:forEach var="p" begin="${requestScope.pageInfo.startPage}"
end="${requestScope.pageInfo.endPage}" step="1"> <!-- 5 -->
<c:if test="${requestScope.pageInfo.pageNo eq p}"> <!-- 6 -->
<button disabled>
<c:out value="${p}"></c:out>
</button>
</c:if>
<c:if test="${requestScope.pageInfo.pageNo ne p}">
<!-- 7 -->
<button class="paging-button">
<c:out value="${p}"></c:out>
</button>
</c:if>
</c:forEach>
<c:if test="${requestScope.pageInfo.pageNo >= requestScope.pageInfo.maxPage}"> <!-- 8 -->
<button disabled>></button>
</c:if>
<c:if test="${requestScope.pageInfo.pageNo < requestScope.pageInfo.maxPage}"> <!-- 9 -->
<button id="nextPage">></button>
</c:if>
<button id="maxPage">>></button> <!-- 10 -->
</c:when>
<c:otherwise> <!-- 검색시 페이징 -->
<button id="searchStartPage"><<</button> <!-- 11 -->
<c:if test="${requestScope.pageInfo.pageNo <= 1}"> <!-- 12 -->
<button disabled><</button>
</c:if>
<c:if test="${requestScope.pageInfo.pageNo > 1}"> <!-- 13 -->
<button id="searchPrevPage"><</button>
</c:if>
<c:forEach var="p" begin="${requestScope.pageInfo.startPage}"
end="${requestScope.pageInfo.endPage}" step="1"> <!-- 14 -->
<c:if test="${requestScope.pageInfo.pageNo eq p}"> <!-- 15 -->
<button disabled>
<c:out value="${p}"></c:out>
</button>
</c:if>
<c:if test="${requestScope.pageInfo.pageNo ne p}"> <!-- 16 -->
<button class="search-paging-button">
<c:out value="${p}"></c:out>
</button>
</c:if>
</c:forEach>
<c:if
test="${requestScope.pageInfo.pageNo >= requestScope.pageInfo.maxPage}"> <!-- 17 -->
<button disabled>></button>
</c:if>
<c:if
test="${requestScope.pageInfo.pageNo < requestScope.pageInfo.maxPage}"> <!-- 18 -->
<button id="searchNextPage">></button>
</c:if>
<button id="searchMaxPage">>></button> <!-- 19 -->
</c:otherwise>
</c:choose>
</div>
<!-- 검색 폼 -->
<form id="searchForm">
<div class="search-area" align="center"> <!-- 20 -->
<select id="searchCondition" name="type">
<option value="writer"<c:if test="${requestScope.pageInfo.type eq 'writer' }">selected</c:if>>작성자</option>
<option value="title" <c:if test="${requestScope.pageInfo.type eq 'title' }">selected</c:if>>제목</option>
<option value="text" <c:if test="${requestScope.pageInfo.type eq 'text' }">selected</c:if>>내용</option>
</select>
<input type="search" id="searchValue" name="keyword" value="${requestScope.pageInfo.keyword }">
<button type="button" id="search">검색하기</button>
</div>
</form>
</div>
</div>
<!-- detail 페이지 이동 js -->
<script type="text/javascript">
const PAGING_PATH = "${pageContext.servletContext.contextPath}/board";
$(function(){
/*list에서 게시물 클릭 시 해당 게시물 상세 페이지로 이동*/
$(".board-tbody > tr").on("click", function(){
let number = $(this).children().eq(0).text(); /*board number*/
location.href = "${pageContext.servletContext.contextPath}/board/" + number;
})
// << 버튼 처음 페이지로 이동
$("#startPage").on("click", function(){
location.href = PAGING_PATH + "?currentPage=1";
})
// < 버튼 이전 페이지로 이동
$("#prevPage").on("click", function(){
location.href = PAGING_PATH + "?currentPage=${reqeustScope.pageInfo.pageNo - 1}";
})
// > 이 후 페이지로 이동
$("#nextPage").on("click", function(){
location.href = PAGING_PATH + "?currentPage=${requestScope.pageInfo.pageNo + 1}";
})
// >> 끝 페이지로 이동
$("#maxPage").on("click", function(){
location.href = PAGING_PATH + "?currentPage=${requestScope.pageInfo.maxPage}";
})
// 페이징 버튼 클릭(1,2,3,4,5...) 해당 페이지로 이동
$(".paging-button").on("click", function(){
let pageNumber = $(this).text();
location.href = PAGING_PATH +"?currentPage="+pageNumber;
})
//작성 버튼 클릭 시 페이지 이동
$("#write").on("click", function(){
location.href = "${pageContext.servletContext.contextPath}/write";
})
// 검색 버튼 클릭 시
$("#search").on("click", function(){
let $type = $("#searchCondition").children(":selected");
let $keyword = $("#searchValue").val();
if($keyword == "") {
alert("검색어를 입력해 주세요")
}else{
$("#searchForm").attr("action", PAGING_PATH);
$("#searchForm").attr("method", "get");
$("#searchForm").submit();
}
})
// 검색 페이징 << 버튼 처음 페이지로 이동
$("#searchStartPage").on("click", function(){
location.href = PAGING_PATH + "?currentPage=1&type=${requestScope.pageInfo.type}&keyword=${requestScope.pageInfo.keyword}";
})
// 검색 페이징 < 버튼 이전 페이지로 이동
$("#searchPrevPage").on("click", function(){
location.href = PAGING_PATH + "?currentPage=${requestScope.pageInfo.pageNo - 1}&type=${requestScope.pageInfo.type}&keyword=${requestScope.pageInfo.keyword}";
})
//검색 페이징 페이지(1,2,3,4,5) 클릭 시 해당 페이지 이동
$(".search-paging-button").on("click", function(){
let pageNumber = $(this).text();
location.href = PAGING_PATH + "?currentPage=" + pageNumber +"&type=${requestScope.pageInfo.type}&keyword=${requestScope.pageInfo.keyword}";
})
//검색 페이징 > 버튼 이 후 페이지로 이동
$("#searchNextPage").on("click", function(){
location.href= PAGING_PATH + "?currentPage=${requestScope.pageInfo.pageNo + 1}&type=${requestScope.pageInfo.type}&keyword=${requestScope.pageInfo.keyword}";
})
//검색 페이징 >> 버튼 끝 페이지로 이동
$("#searchMaxPage").on("click", function(){
location.href = PAGING_PATH + "?currentPage=${requestScope.pageInfo.maxPage}&type=${requestScope.pageInfo.type}&keyword=${requestScope.pageInfo.keyword}";
})
//전체페이지
$("#all-button").on("click", function(){
location.href = PAGING_PATH
})
})
</script>
</body>
</html>
브라우저에서 확인해보도록 하겠습니다.
해당 전체 리스트를 조회하는 페이지에서 작성자(type : writer) , 검색어(keyword : sj)를 입력하여 검색해 보도록 하겠습니다.
페이지를 넘겼을 때 검색이 유지되는지 확인해 보도록 하겠습니다.
검색이 유지가 되는 것을 확인할 수 있습니다.