- 관리자 페이지를 구현하여 회원 및 게시판을 관리할 수 있도록 구현해보려고 합니다.
- 오늘은 우선 회원 관리 페이지를 구현하여 해당 사이트에 회원가입한 사용자들을 확인하고 해당 사용자가 작성한 게시글과 댓글을 확인할 수 있도록 하였습니다.
- 추가로 관리자의 권한으로 특정 사용자를 탈퇴시킬 수 있도록 구현해보았습니다.
📝 회원 관리 페이지
- 우선 관리자 페이지 경로를 요청받는 새로운 컨트롤러를 작성해주었습니다.
package com.cos.blog.controller;
...
@RequiredArgsConstructor
@Controller
public class AdminController {
private final UserRepository userRepository;
@GetMapping("/admin")
public String admin(Model model,
@PageableDefault(size = 12, sort = "id", direction = Sort.Direction.DESC) Pageable pageable,
@RequestParam(value = "category", defaultValue = "user") String category,
@RequestParam(value = "searchType", defaultValue = "") String searchType,
@RequestParam(value = "searchKeyword", required = false) String searchKeyword,
@AuthenticationPrincipal PrincipalDetail principal) {
/* 관리자 권한이 아닌 경우 해당 페이지를 요청하지 못하도록 설정 */
if(!principal.getUser().getRole().equals(RoleType.ADMIN)) {
return null;
}
/* Specification을 사용하여 쿼리 조건 추가 */
Specification<User> spec = (root, query, criteriaBuilder) -> null;
spec = spec.and(AdminSpecification.userRole(RoleType.USER));
if(category.equals("user")) {
if(!searchType.isEmpty()) {
if(searchType.equals("username")) {
spec = spec.and(AdminSpecification.searchTypeUsername(searchKeyword));
} else {
spec = spec.and(AdminSpecification.searchTypeNickname(searchKeyword));
}
}
model.addAttribute("users", userRepository.findAll(spec, pageable));
}
model.addAttribute("category", category);
return "admin";
}
}
- 해당 컨트롤러에서는 이전 포스팅과 마찬가지로 Specification을 사용하여 쿼리 조건을 추가하고 있으며 여기서 사용한 AdminSpecification 클래스는 아래와 같이 구현되어있습니다.
package com.cos.blog.specification;
...
public class AdminSpecification {
public static Specification<User> searchTypeUsername(String searchKeyword) {
return (root, query, criteriaBuilder) -> criteriaBuilder.like(root.get("username"), "%" + searchKeyword + "%");
}
public static Specification<User> searchTypeNickname(String searchKeyword) {
return (root, query, criteriaBuilder) -> criteriaBuilder.like(root.get("nickname"), "%" + searchKeyword + "%");
}
public static Specification<User> userRole(RoleType role) {
return (root, query, criteriaBuilder) -> criteriaBuilder.equal(root.get("role"), role);
}
}
- 각각의 함수는 Username, Nickname, RoleType에 따라 데이터를 필터링하여 가져오는 역할을 수행합니다.
- 또한 위 클래스를 사용하기 위해 아래와 같이 Repository 클래스에서 JpaSpecificationExcutor 클래스를 상속받도록 해주었습니다.
package com.cos.blog.repository;
...
// @Repository
public interface UserRepository extends JpaRepository<User, Long>, JpaSpecificationExecutor<User>{
...
}
- 간단히 테스트해보면 아래와 같은 결과가 출력됩니다.
📝 작성 게시물 모달
- 다음으로는 위와 같은 회원 관리 페이지에서 회원의 게시글수를 클릭하게 되면 아래와 같은 모달을 띄워 해당 사용자가 작성한 게시물들을 볼 수 있도록 구현하였습니다.
- 이를 위해, 버튼 클릭시 아래와 같은 ajax 요청을 수행하도록 구현해주었습니다.
function modalOpen(type, nickname, user_id) {
if(type == "board") {
$("#modalTitle").text(nickname + "님의 작성 게시물");
let item =
`<tr>
<th class="board-table-no">번호</th>
<th class="board-table-title">제목</th>
<th class="board-table-date">작성일</th>
<th class="board-table-view">조회수</th>
<th class="board-table-recommend">추천수</th>
</tr>`
$.ajax({
url: `/api/board/${user_id}`,
dataType: "json"
}).done(resp => {
resp.forEach((board) => {
item += getBoardModalItem(board);
});
$("#modalTableBody").append(item);
}).fail(error => {
console.log(error);
});
} else {
$("#modalTitle").text(nickname + "님의 작성 댓글");
let item =
`<tr>
<th class="board-table-no">번호</th>
<th class="board-table-title">내용</th>
<th class="board-table-date">작성일</th>
</tr>`
$.ajax({
url: `/api/reply/${user_id}`,
dataType: "json"
}).done(resp => {
resp.forEach((reply) => {
item += getReplyModalItem(reply);
});
$("#modalTableBody").append(item);
}).fail(error => {
console.log(error);
});
}
}
function getBoardModalItem(board) {
let item =
`<tr onclick="location.href='/board/${board.id}/'">
<th>${board.id}</th>
<th class="board-table-title">${board.title}</th>
<th>${board.create_date}</th>
<th>${board.views}</th>
<th>${board.recommends}</th>
</tr>`;
return item;
}
function getReplyModalItem(reply) {
let item =
`<tr>
<th>${reply.id}</th>
<th class="board-table-title">${reply.content}</th>
<th>${reply.create_date}</th>
</tr>`;
return item;
}
function modalClose() {
$("#modalTableHead > tr").remove();
$("#modalTableBody > tr").remove();
}
- 이후 위 ajax 요청을 받을 수 있도록 아래와 같은 함수들을 컨트롤러와 서비스 단에 각각 구현해주었습니다.
@GetMapping("/api/board/{user_id}")
public ResponseEntity<?> findByUser(@PathVariable("user_id") Long id) {
List<BoardModalDto> boardModalDtoList = boardService.findByUser(id);
return new ResponseEntity<>(boardModalDtoList, HttpStatus.OK);
}
@Transactional(readOnly = true)
public List<BoardModalDto> findByUser(Long user_id) {
List<Board> boards = boardRepository.findByUserId(user_id);
List<BoardModalDto> boardModalDtoList = new ArrayList<>();
for(int i = 0; i < boards.size(); i++) {
BoardModalDto boardModalDto = BoardModalDto.builder()
.id(boards.get(i).getId())
.title(boards.get(i).getTitle())
.create_date(boards.get(i).getCreateDate())
.views(boards.get(i).getCount())
.recommends(boards.get(i).getRecommendCount())
.build();
boardModalDtoList.add(boardModalDto);
}
return boardModalDtoList;
}
- 여기서 boardModalDto는 아래와 같은 필드로 구성되어 있습니다.
package com.cos.blog.dto;
...
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Data
public class BoardModalDto {
private Long id;
private String title;
private String create_date;
private Integer views;
private Integer recommends;
}
- 게시글과 마찬가지로 댓글 역시 위와 같이 동작하도록 구현해주었으며 구현 방식은 동일합니다.
📝 View
- View 페이지는 아래와 같이 구현해주었습니다.
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<!-- 헤더, 사이드바 레이아웃 설정 -->
<%@ include file="layout/header.jsp"%>
<%@ include file="layout/sidebar.jsp"%>
<!-- 헤더, 사이드바 레이아웃 설정 끝 -->
<div id="content">
<c:if test="${category eq 'user'}">
<div class="board-title">| 회원 관리</div>
<!-- 정렬 및 검색 탭 -->
<form action="/admin" method="GET" class="form-inline bd-highlight justify-content-between">
<div></div>
<div>
<select id="select" class="form-control" onchange="selectSearchType()">
<option value="username">아이디</option>
<option value="nickname">닉네임</option>
</select>
<input type="hidden" name="category" id="category" value="${param.category}">
<input type="hidden" name="searchType" id="searchType" value="username">
<input type="text" name="searchKeyword" id="searchKeyword" class="form-control" placeholder="입력">
<button type="submit" class="btn btn-search">
<i class="fa-solid fa-magnifying-glass"></i> 검색
</button>
</div>
</form>
<!-- 정렬 및 검색 탭 끝 -->
<!-- 회원 관리 탭 -->
<table class="table board-table table-hover">
<thead>
<tr>
<th class="board-table-no">번호</th>
<th class="board-table-writer">아이디</th>
<th class="board-table-writer">닉네임</th>
<th class="board-table-date">이메일</th>
<th class="board-table-date">가입일</th>
<th class="board-table-no">게시글수</th>
<th class="board-table-no">댓글수</th>
<th class="board-table-no">관리</th>
</tr>
</thead>
<tbody>
<c:forEach var="user" items="${users.content}">
<tr>
<th>${user.id}</th>
<th>${user.username}</th>
<th>${user.nickname}</th>
<th>${user.email}</th>
<th>${user.createDate}</th>
<th>
<button type="button" class="btn-admin-modal" data-toggle="modal" data-target="#modal" onclick="modalOpen('board', '${user.nickname}', ${user.id})">
${user.boardCount}
</button>
</th>
<th>
<button type="button" class="btn-admin-modal" data-toggle="modal" data-target="#modal" onclick="modalOpen('reply', '${user.nickname}', ${user.id})">
${user.replyCount}
</button>
</th>
<th><button class="btn btn-admin" onclick="userKick(${user.id})">회원 추방</button></th>
</tr>
</c:forEach>
</tbody>
</table>
<!-- 회원 관리 탭 끝 -->
<!-- 페이징 -->
<c:set var="startPage" value="${users.number - users.number % 5}" />
<ul class="pagination justify-content-center">
<li class="page-item <c:if test='${users.number < 5}'>disabled</c:if>">
<a class="page-link" href="/admin?category=${param.category}&page=${startPage - 5}&searchType=${param.searchType}&searchKeyword=${param.searchKeyword}"><</a>
</li>
<c:forEach var="page" begin="1" end="5">
<c:if test="${(startPage + page) <= users.totalPages}">
<li class="page-item <c:if test='${users.number eq startPage + page - 1}'>active</c:if>">
<a class="page-link" href="/admin?category=${param.category}&page=${startPage + page - 1}&searchType=${param.searchType}&searchKeyword=${param.searchKeyword}">${startPage + page}</a>
</li>
</c:if>
</c:forEach>
<li class="page-item <c:if test='${startPage + 5 > users.totalPages}'>disabled</c:if>">
<a class="page-link" href="/admin?category=${param.category}&page=${startPage + 5}&searchType=${param.searchType}&searchKeyword=${param.searchKeyword}">></a>
</li>
</ul>
<!-- 페이징 끝 -->
</c:if>
</div>
</div>
<!-- 게시글 수 모달 -->
<div class="modal fade" id="modal">
<div class="modal-dialog modal-lg modal-dialog-scrollable">
<div class="modal-content">
<!-- Modal Header -->
<div class="modal-header">
<h4 class="modal-title" id="modalTitle"></h4>
<button type="button" class="close" data-dismiss="modal">×</button>
</div>
<!-- Modal body -->
<div class="modal-body" id="modalBody">
<table class="table board-table table-hover">
<thead id="modalTableHead"></thead>
<tbody id="modalTableBody"></tbody>
</table>
</div>
<!-- Modal footer -->
<div class="modal-footer">
<button type="button" class="btn btn-danger" data-dismiss="modal" onclick="modalClose()">Close</button>
</div>
</div>
</div>
</div>
<!-- 게시글 수 모달 끝 -->
<!-- 스크립트 설정 -->
<script src="/js/board.js"></script>
<script src="/js/user.js"></script>
<script src="/js/admin.js"></script>
<!-- 스크립트 설정 끝 -->
<!-- 푸터 레이아웃 설정 -->
<%@ include file="layout/footer.jsp"%>
<!-- 푸터 레이아웃 설정 끝 -->
- 참고로 모달창의 경우 부트스트랩을 통해 구현하였습니다.
'🚗 Backend Toy Project > 스프링 부트 게시판' 카테고리의 다른 글
[스프링부트 게시판] 37. 비밀번호 찾기 (0) | 2022.11.01 |
---|---|
[스프링부트 게시판] 36. 관리자 페이지 - 게시글 통계 (0) | 2022.10.26 |
[스프링부트 게시판] JPA Specification을 통해 쿼리 조건 다루기 (0) | 2022.10.06 |
[스프링부트 게시판] 34. 사용자 프로필 이미지 추가 (0) | 2022.09.27 |
[스프링부트 게시판] 33. 댓글 알림 기능 (1) | 2022.09.23 |