4. Thymeleaf 유저페이지, JPA로 검색 기능 구현

wjd15sheep·2025년 7월 16일
0

Spring Boot

목록 보기
19/19

이번 포스트에서는 user 페이지에 남아 있던 검색 기능, 페이지네이션, 코드 리팩토링을 마무리하고, 최종적으로 불필요한 form 페이지와 관련 코드를 정리합니다.

최종 완성된 결과는 아래와 같이 나타납니다.

최종결과 이미지

🎯 이번 포스트 목표

  • 페이지네이션 기능 구현
  • 검색 기능 처리
  • 불필요한 form 관련 코드 제거
  • 전체 유저 페이지 코드 리팩토링

🔥 기존 form 페이지 정리

form.html 페이지와 관련된 컨트롤러, 서비스 코드는 모두 삭제합니다.
해당 페이지는 form 태그를 다루는 학습용 예제였기 때문에, 이제 실사용에 포함되지 않습니다.


🧩 페이지네이션(Pagination) 기능 구현

1. user.html에 페이지네이션 UI 추가

먼저 Bootstrap에서 기본 제공하는 pagination 코드를 활용해 UI를 구성합니다.

<nav aria-label="Page navigation example">
  <ul class="pagination">
    <li class="page-item"><a class="page-link" href="#">Previous</a></li>
    <li class="page-item"><a class="page-link" href="#">1</a></li>
    <li class="page-item"><a class="page-link" href="#">2</a></li>
    <li class="page-item"><a class="page-link" href="#">3</a></li>
    <li class="page-item"><a class="page-link" href="#">Next</a></li>
  </ul>
</nav>

2. 컨트롤러에서 Pageable 받기

페이지네이션 처리를 위해 @PageableDefault 어노테이션을 사용해 페이징 정보를 컨트롤러에서 바로 받을 수 있습니다.

    @GetMapping("/users")
    public String userView(Model model, @PageableDefault(size = 20) Pageable pageable) {
            UserPageDto userPageDto = userService.getAllUsers(pageable);

    model.addAttribute("startPage", userPageDto.getStartPage());
    model.addAttribute("endPage", userPageDto.getEndPage());
    model.addAttribute("users", userPageDto.getUsers());
    return "/user/user";
    }

📌 참고

  • Pageable은 page, size, sort를 기본으로 처리합니다.
  • @PageableDefault(size = 20)를 통해 기본 한 페이지 크기를 20으로 지정할 수 있습니다.
  • 예시 URL: http://localhost:8080/admin/users?page=1&size=1

3. 서비스에서 JPA와 연동

JPA에서는 Pageable을 findAll()에 그대로 넘겨주기만 하면 됩니다.
이를 통해 자동으로 LIMIT, OFFSET 처리가 이루어집니다.

// UserService
 public UserPageDto getAllUsers(Pageable pageable) {
        
        Page<User> users = userRepository.findAll(pageable);
        return UserPageDto.of(users);
    }

4. 페이지 정보 담을 DTO 생성

페이지 정보와 함께 사용자 데이터를 담는 DTO입니다.
기본 구조는 다음과 같습니다.

//UserPageDto
@Getter
@AllArgsConstructor(access = AccessLevel.PRIVATE)
public class UserPageDto {

    private Page<User> users;
    private int startPage;
    private int endPage;

    public static UserPageDto of(Page<User> users) {
        int currentPage = users.getNumber();
        int totalPages = users.getTotalPages();

        int startPage = Math.max(1, currentPage - 4);
        int endPage = Math.min(totalPages, currentPage + 4);

        return new UserPageDto(users, startPage, endPage);
    }
}

📝 여기선 startPage, endPage 등 페이지 범위 정보도 포함할 수 있도록 확장 가능합니다.

5. 🧪 HTML에서 Page 사용하기 (Thymeleaf 방식)

HTML에서 Page 객체를 다루기 위해선 다음처럼 접근하면 됩니다:

  • 전체 데이터 개수: users.totalElements
  • 현재 페이지 번호: users.pageable.pageNumber + 1
  • 전체 페이지 수: users.totalPages
  • 페이지는 0부터 시작하므로, 화면에 표시할 땐 +1을 해줍니다.

🔁 HTML 페이지네이션 반복 코드 (Thymeleaf)


    @GetMapping("/users")
    public String userView(Model model, @PageableDefault(size = 20) Pageable pageable ) {
        UserPageDto userPageDto = userService.getAllUsers(pageable);

        model.addAttribute("users", userPageDto.getUsers());
        return "/user/user";
    }

user.html

      <div class="text-end">
        총 건수 : <span th:text="${users.totalElements}">0</span>
      </div>
...

 <nav aria-label="Page navigation example">
        <ul class="pagination justify-content-center">
          <li class="page-item">
            <a
              class="page-link"
              href="#"
              th:classappend="${1 == users.pageable.pageNumber + 1} ? 'disabled'"
              th:href="@{/admin/users(page=${users.pageable.pageNumber - 1}, searchText = ${param.searchText}, sort=${sort})}"
              >&lt;</a
            >
          </li>
          <li
            class="page-item"
            th:classappend="${i == users.pageable.pageNumber + 1} ? 'disabled'"
            th:each="i : ${#numbers.sequence(startPage, endPage)}"
          >
            <a
              class="page-link"
              href="#"
              th:text="${i}"
              th:href="@{/admin/users(page=${i - 1}, searchText = ${param.searchText}, sort=${sort})}"
              >1</a
            >
          </li>
          <li
            class="page-item"
            th:classappend="${users.totalPages == users.pageable.pageNumber + 1} ? 'disabled'"
          >
            <a
              class="page-link"
              href="#"
              th:href="@{/admin/users(page=${users.pageable.pageNumber + 1}, searchText = ${param.searchText}, sort=${sort})}"
              >&gt;</a
            >
          </li>
        </ul>
      </nav>

이와 같은 코드로 페이지네이션 구현 끝


🔎 검색 기능 연동하기, 정렬 (선택적으로 확장 가능)

이번에는 user 페이지에 검색 기능과 정렬 기능을 연동해보겠습니다.
검색어를 입력하고 정렬 기준을 바꿔도 페이지 이동 시 해당 정보가 유지되도록 구현할 것입니다.

1. 검색/정렬 폼 추가

GET 방식의 form을 사용해 검색어와 정렬 기준을 유지한 채로 페이지를 이동할 수 있게 구성합니다.

  <form
        class="row g-3 d-flex justify-content-center"
        method="GET"
        th:action="@{/admin/users}"
      >
        <div class="col-4">
          <label for="searchText" class="visually-hidden">검색</label>
          <input
            type="text"
            class="form-control"
            id="searchText"
            name="searchText"
            th:value="${param.searchText}"
          />
        </div>
        <div class="col-auto">
          <select class="form-select" name="sort" onchange="this.form.submit()">
            <option th:value="idAsc" th:selected="${sort} == 'idAsc'">ID 오름차순</option>
            <option th:value="idDesc" th:selected="${sort} == 'idDesc'">ID 내림차순</option>
            <option th:value="nameAsc" th:selected="${sort} == 'nameAsc'">이름 오름차순</option>
          </select>
        </div>
        <div class="col-auto">
          <button type="submit" class="btn btn-dark mb-3">검색</button>
        </div>
        <div class="col-auto">
          <a class="btn btn-secondary mb-3" href="/admin/users">초기화</a>
        </div>
      </form>
  

2. 컨트롤러에서 파라미터 받기

검색어(searchText)와 정렬 기준(sort)을 @RequestParam으로 받아 처리합니다.

@GetMapping("/users")
public String userView(
  Model model, 
  @PageableDefault(size = 20) Pageable pageable,
  @RequestParam(required = false, defaultValue = "") 
  String searchText, 
  @RequestParam(defaultValue = "idAsc") String sort) {
        UserPageDto userPageDto = userService.getAllUsers(pageable, searchText, sort);

        model.addAttribute("startPage", userPageDto.getStartPage());
        model.addAttribute("endPage", userPageDto.getEndPage());
        model.addAttribute("users", userPageDto.getUsers());
        model.addAttribute("sort", sort);
        return "/user/user";
    }

3. 서비스에서 검색 + 정렬 처리

  
  public UserPageDto getAllUsers(Pageable pageable, String searchText, String sort) {
    Pageable sortedPageable = PageRequest.of(
        pageable.getPageNumber(),
        pageable.getPageSize(),
        getSortOption(sort)
    );

    Page<User> users = userRepository.findAllByNameContainingOrEmailContainingOrMobileNumberContainingOrAddressContaining(
        searchText, searchText, searchText, searchText, sortedPageable
    );

    return UserPageDto.of(users);
}

정렬 기준은 Map으로 깔끔하게 관리합니다.


  private Sort getSortOption(String sort) {
    Map<String, Sort> sortMap = Map.of(
        "idAsc", Sort.by(Sort.Direction.ASC, "id"),
        "nameAsc", Sort.by(Sort.Direction.ASC, "name"),
        "idDesc", Sort.by(Sort.Direction.DESC, "id") // 기본값
    );

    return sortMap.getOrDefault(sort, Sort.by(Sort.Direction.DESC, "id"));
}

📌 검색 쿼리 설명

Spring Data JPA의 메서드 이름 쿼리 방식입니다. 아래 조건 중 하나라도 해당하면 데이터를 조회합니다:

  • 이름(name)
  • 이메일(email)
  • 전화번호(mobileNumber)
  • 주소(address)
`SELECT *
FROM user
WHERE name LIKE '%searchText%'
   OR email LIKE '%searchText%'
   OR mobile_number LIKE '%searchText%'
   OR address LIKE '%searchText%'`

✅ 마무리

이제 유저 페이지에서 다음 기능이 완벽히 동작합니다:

  • 검색어 필터링
  • 정렬 기준 선택
  • 페이지네이션
  • 불필요한 코드 제거 및 리팩토링
profile
성장 위해 노력하는 웹 개발자 주니어

0개의 댓글