국비/Project - 1 게시판

2023.04.28 66일차 Project

춘핑이 2023. 5. 1. 08:13

66일차 Project

9. 프로젝트 페이지네이션

프로젝트에 페이지네이션을 적용해보자.

페이지네이션을 하기 위해 일단 데이터를 추가해준다.
테이블을조회해서 추가하는것이라 있는만큼 계속 추가해준다.

INSERT INTO Board (title, body, writer) 
(SELECT title, body, writer FROM Board);

9.1 컨트롤러

page파라미터를 받기 위해서 추가해준다. 처음에는 기본값으로 값이 없으니 1페이지를 제공한다.
기존 메소드를 오버로딩해서 만들자.

@GetMapping({ "/", "list" })
public String list(Model model,@RequestParam(value = "page", defaultValue = "1") Integer page) {
    List<Board> list = service.listBoard(page);

    model.addAttribute("boardList", list);
    return "list";
}

9.2 서비스 - 1

서비스의 메소드에서 DB데이터를 받아서 가공하는 일을 하는 것이다.
mapper에게 필요한 정보를 건네주고 DB를 달라고 해야한다.
startIndex번호를 건네준다.

@Override
public List<Board> listBoard(Integer page) {
    Integer size = 15;
    Integer startIndex = (page - 1 ) * size;

    //게시물 목록

    //페이지 네이션이 필요한 정보
    return mapper.selectAllPaging(startIndex, size);
}

9.3 매퍼

id기준으로 내림차순 정렬하고 LIMIT를 통해서 갯수를 지정해준다.

@Select("SELECT id, title, writer, inserted FROM Board ORDER BY id DESC LIMIT #{startIndex}, #{size}")
List<Board> selectAllPaging(Integer startIndex, Integer size);

9.4 view

페이지 네이션을 부트스트랩에서 받아와서 넣어주자.

<div class="container-lg">
    <div class="row">
        <nav aria-label="Page navigation example">
          <ul class="pagination justify-content-center">
            <li class="page-item"><a class="page-link" href="#">이전</a></li>
            <c:forEach begin="1" end="10" var="pageNumber">
                <c:url value="/list" var="pageLink">
                    <c:param name="page" value="${pageNumber}"/>
                </c:url>
                <li class="page-item"><a class="page-link" href="${pageLink}">${pageNumber}</a></li>
            </c:forEach>
            <li class="page-item"><a class="page-link" href="#">다음</a></li>
          </ul>
        </nav>
    </div>
</div>

9.5 서비스 - 2

왼쪽값 오른쪽값등등 나머지 필요한 값들을 서비스에서 구현해줘야한다.

마지막 페이지 번호 : (총 글개수 - 1) / rowPerPage + 1
페이지네이션 : 왼쪽 번호 10씩늘어나는 등차수열 1 11 21 31
오른쪽 : 10 20 30

이번엔 기본스타일 말고 구글 스타일로 바꾸어보자.
현재 페이지를 기준으로 +4 -5로 보여준다.

값은 javaBean이나 Map으로 넘겨줘야한다.
이번엔 Map으로 넘겨주자. 게시글 list도 보내야한다.
Map에 Map과 list를 담아서 보내주도록 하자.

view에서는 map의 값을 꺼낼땐 pageInfo.leftPageNumber으로 꺼내면된다.

leftPageNumber가 1페이지보다 작다면 1이 들어가게 해준다.
마지막 페이지를 더 넘어가기 않기 위해서는 오른쪽 값을 lastPageNumber와 비교해서 넣어주면된다.

leftPageNumber = Math.max(leftPageNumber, 1);
rightPageNumber = Math.min(rightPageNumber, lastPageNumber); 

@Override
public Map<String, Object> listBoard(Integer page) {
    Integer rowPerPage = 10;
    Integer startIndex = (page - 1) * rowPerPage;

    // 페이지 네이션이 필요한 정보
    // 전체 레코드 수
    Integer numOfRecords = mapper.countAll();

    // 마지막 페이지 번호 (총 글개수 - 1) / rowPerPage + 1
    Integer lastPageNumber = (numOfRecords - 1) / rowPerPage + 1;

    // 페이지네이션 왼쪽 번호
    Integer leftPageNumber = page - 5;

    // 페이지네이션 오른쪽 번호
    Integer rightPageNumber = leftPageNumber + 9;

    leftPageNumber = Math.max(leftPageNumber, 1);
    rightPageNumber = Math.min(rightPageNumber, lastPageNumber);

    Map<String, Object> pageInfo = new HashMap<>();
    pageInfo.put("leftPageNumber", leftPageNumber);
    pageInfo.put("rightPageNumber", rightPageNumber);

    // 게시물 목록
    List<Board> list = mapper.selectAllPaging(startIndex, rowPerPage);

    return Map.of("pageInfo", pageInfo, "list", list);
}

9.6 active표시

현재 페이지값을 보내고 pageNum과 비교하면된다.

Integer currentPageNumber = page;
pageInfo.put("prevPageNumber", prevPageNumber);

${pageNum eq pageInfo.currentPageNumber ? 'active' : '' }

9.7 이전페이지 다음페이지

현재 페이지 기준으로 -1 +1해주면된다.
각각 1페이지와 마지막 페이지엔 안보이게 해주자.

Integer prevPageNumber = currentPageNumber - 1;

Integer nextPageNumber = currentPageNumber + 1;

<c:if test="${pageInfo.currentPageNumber gt 1}">
    <li class="page-item">
        <c:url value="/list" var="pageLink">
            <c:param name="page" value="${pageInfo.prevPageNumber}"/>
        </c:url> 
        <a class="page-link" href="${pageLink}">이전</a>
    </li>
</c:if>

<c:if test="${pageInfo.currentPageNumber lt pageInfo.lastPageNumber}">
    <li class="page-item">
        <c:url value="/list" var="pageLink">
            <c:param name="page" value="${pageInfo.nextPageNumber}"/>
        </c:url> 
        <li class="page-item"><a class="page-link" href="${pageLink}">다음</a></li>
    </li>
</c:if>

9.8 addAllAttributes

모델에 꺼내서 보내고 꺼내서 보내고 햇엇는데 addAllAttributes메소드를 사용하면 그냥 맵을 보낼 수 있다.

@GetMapping({ "/", "list" })
public String list(Model model, @RequestParam(value = "page", defaultValue = "1") Integer page) {

    Map<String, Object> result = service.listBoard(page); // 페이지 처리후

    //model.addAttribute("boardList", result.get("list"));
    //model.addAttribute("pageInfo", result.get("pageInfo"));
    model.addAllAttributes(result);

    return "list";
}

9.9 이전 다음 버튼

텍스트로 사용해도되는데 이전 버튼과 다음버튼을 아이콘으로 바꿔보자.
font awesome을 사용하자.

<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" integrity="sha512-iecdLmaskl7CVkqkXNQ/ZH/XLlvWZOJyj7Yy7tcenmpD1ypASozpmT/E0iPtmFIB46ZmdtAc9eNBvH0H/ZpiBw==" crossorigin="anonymous" referrerpolicy="no-referrer" />

<i class="fa-solid fa-angle-left"></i>
<i class="fa-solid fa-angle-right"></i>

맨처음으로가기 맨마지막으로가기 버튼
각각 1페이지 마지막페이지로 이동하면된다.

<c:if test="${pageInfo.currentPageNumber gt 1}">
    <li class="page-item">
        <c:url value="/list" var="pageLink">
            <c:param name="page" value="1"/>
        </c:url> 
        <a class="page-link" href="${pageLink}">
            <i class="fa-solid fa-angles-left"></i>
        </a>
    </li>
</c:if>

<c:if test="${pageInfo.currentPageNumber lt pageInfo.lastPageNumber}">
    <li class="page-item">
        <c:url value="/list" var="pageLink">
            <c:param name="page" value="${pageInfo.lastPageNumber}"/>
        </c:url> 
        <a class="page-link" href="${pageLink}">
            <i class="fa-solid fa-angles-right"></i>
        </a>
    </li>
</c:if>

10. 정적파일

페이지에 로고 등 정적파일을 컨트롤러를 거치지 않고 바로 제공해야하는 경우가 있다.
폴더 구조에 java resources에 static파일이 있다.
static파일에 넣어두면 컨트롤러를 거치지 않고 볼 수 있다.
static이라는 루트에 들어가는 느낌이다.

<a class="navbar-brand" href="/list">
    <img alt="projcet-1" src="/img/spring-logo.png" height="25" style="margin-bottom: 5px">
</a> <!-- 프로젝트 브랜드 -->

11. Dynamic SQL

쿼리가 복잡해질 경우에 어떻게 해야하는지 알아보자.
마이바테스 - Dynamic SQL를 보러가자 동적 sql
배열과 같은 값이 sql에 들어가서 값이 들어가는것이다.

if choose(when ohterwise) trim(where set) foreach가 있다.

어떤 조건에 의해서 쿼리의 일부분이 붙거나 안붙거나 등의 일이 하고 싶다.

이런 것들을 사용하고 싶다면 어노테이션이 아니라 XML을 만들어서 사용해야한다.
다행이도 어노테이션에서도 이것을 사용할 수 잇다.
script 태그를 sql문 앞뒤로 어노테이션에 넣으면 된다.

보통은 어노테이션이 +연산을 사용해야해서 훨씬 어려운데 텍스트블록이 생기면서 쉬워졋다.
자바 15버전 부터 가능하다
그래서 큰그림에서만 이해하도록 하자.

SELECT * FROM Customers
WHERE CustomerName LIKE '%ell%';

와 같은 sql을 짜고싶다.

사용자는 %%사이에 쿼리를 집어넣어야한다.

마이바티스에서 쿼리에 #{}으로 들어가면 LIKE 'ELL'으로 들어가게 된다.
<bind name="pattern" value="keyword"> bind태그를 사용하면 매핑된 이름으로 값이 그대로 들어가게 된다.

Select("""
        <script>
        <bind name="pattern" value="'%' + keyword + '%' "/>
        SELECT customerId, customerName, contactName, address 
        FROM Customers 
        WHERE CustomerName LIKE #{pattern}
        ORDER BY customerId DESC
        </script>
        """)
List<Customer> sql1(String keyword);

// 경로 : /sub27/link1?s=ell
@GetMapping("link1")
public String method1(@RequestParam(value = "s", defaultValue = "") String keyword, Model model) {
    model.addAttribute("customerList", mapper.sql1(keyword));  
    return "/sub13/link1";
}

11.2 Dynamic SQL- 2

직원 lastName이나 firstName에 있으면 검색하기 steven
/sub27/link2?search=? 경로로 요청시 조회하기

@Select("""
        <script>
        <bind name="pattern" value="'%' + keyword + '%'"/>
        SELECT employeeId, lastName, firstName
        FROM Employees 
        WHERE lastName LIKE #{pattern} OR firstName LIKE #{pattern}
        ORDER BY employeeId DESC
        </script>
        """)
List<Employee> sql2(String keyword);

// 경로 : /sub27/link2?search=steven
@GetMapping("link2")
public String method2(@RequestParam(value = "s", defaultValue = "") String keyword, Model model) {
    model.addAttribute("employeeList", mapper.sql2(keyword));
    return "/sub13/link2";
}

11.3 Dynamic SQL- 3

프로젝트에 검색기능을 추가해보자.

11.3.1 view

검색폼에 list로 가게 해주자.

<form action="/list" class="d-flex" role="search">
    <input name="search" class="form-control me-2" type="search" placeholder="Search" aria-label="Search">
    <button class="btn btn-outline-success" type="submit">
        <i class="fa-solid fa-magnifying-glass"></i>
    </button>
</form>

11.3.2 controller

제목 또는 본문 또는 작성자 이름에 있으면 검색되도록 변경해주자.
컨트롤러에는 String search파라미터를 넣어주면된다.
listBoard를 하나의 메소드로 만들것인지 오버로드 할것인지 선택해야한다.

@GetMapping({ "/", "list" })
public String list(Model model, 
        @RequestParam(value = "page", defaultValue = "1") Integer page,
        @RequestParam(value = "search", defaultValue = "") String search) {

    Map<String, Object> result = service.listBoard(page, search); // 페이지 처리후

    model.addAllAttributes(result);
    return "list";
}

11.3.3 service

String search파라미터를 매퍼에게 전달해주면된다.

11.3.4 mapper

sql 을 script태그 안에 넣고 bind로 앞뒤 %를 붙여주자.

@Select("""
        <script>
        <bind name="pattern" value="'%' + search + '%'"/>
        SELECT id, title, writer, body, inserted 
        FROM Board 
        WHERE title LIKE #{pattern}
        OR writer LIKE #{pattern}
        OR body LIKE #{pattern}
        ORDER BY id DESC 
        LIMIT #{startIndex}, #{rowPerPage}
        </script>
        """)
List<Board> selectAllPaging(Integer startIndex, Integer rowPerPage, String search);

11.3.5 파생되는 문제

1.검색 후에 다음 페이지로 가면 값이 남아있지 않다.
검색 input박스의 value에 param.search을 넣어주면된다.

2.페이지 넘어가도 검색 유지
페이지 파라미터에 붙여서 넘겨주면된다.
url패턴에 search파라미터를 남겨주자.
검색안해도 있는 것은 정신없으니 비어잇을때만 들어가도록 해주자.

<c:if test="${pageInfo.currentPageNumber lt pageInfo.lastPageNumber}">
    <li class="page-item">
        <c:url value="/list" var="pageLink">
            <c:param name="page" value="${pageInfo.nextPageNumber}"/>
            <c:if test="${not empty param.search }">
                <c:param name="search" value="${param.search}"/>
            </c:if>
        </c:url> 
        <a class="page-link" href="${pageLink}">
            <i class="fa-solid fa-angle-right"></i>
        </a>
    </li>
</c:if>

3.조회후 마지막 페이지
조회후 마지막 페이지가 전부있는게 아니라 변경을 해줘야한다.
총 게시글 수를 조회하는 쿼리도 조건에 따라 변하게 해줘야한다.

@Select("""
        <script>
        <bind name="pattern" value="'%' + search + '%'"/>
        SELECT COUNT(id) count 
        FROM Board
        WHERE title LIKE #{pattern}
        OR writer LIKE #{pattern}
        OR body LIKE #{pattern}
        </script>
        """)
Integer countAll(String search);

11.4 Dynamic SQL- 4

JSTL과 비슷하게 사용할 수 있다.
if / foreach
true이면 sql이 포함되도록하자.

application.properties에
logging.level.com.example.demo.mapper=debug을 추가하면 간단하게 쿼리 내용을 볼 수 있다.

JSTL처럼 IF태그를 사용하면 조건에 맞게 값이 들어가게 된다.

@Select("""
        <script>
        SELECT COUNT(*)
        FROM Customers
        <if test="true">
        WHERE CustomerID =10
        </if>
        </script>
        """)
Integer sql3();

@Select("""
        <script>
        SELECT COUNT(*)
        FROM Customers
        <if test="keyword != null">
        <bind name="pattern" value="'%' + keyword + '%'"/>
        WHERE CustomerName LIKE #{pattern}
        </if>
        </script>
        """)
Integer sql4(String keyword);

if태그에 넣을 수 있는 연산식은 OGNL문법을 사용하고 있다.
OGNL는 아파치에서 공식 문서를 볼 수 있다.
https://commons.apache.org/proper/commons-ognl/ Language Guide를 보자.

== eq
!=neq
<= gte
등등 이 있다. 필요할때 문서를 보고 사용하자.

2023.04.30

logging.level.com.example.demo.mapper=debug를 이용해서 sql문의 완성 결과를 간단하게 볼 수 있다.