Skip to content

Commit 1ebdb26

Browse files
authored
feat:관리자 계정 목록 조회 구현 (#567)
1 parent e172b89 commit 1ebdb26

11 files changed

Lines changed: 402 additions & 4 deletions

File tree

src/main/java/org/ject/support/admin/account/controller/AdminAccountController.java

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,20 +5,32 @@
55
import io.swagger.v3.oas.annotations.tags.Tag;
66
import jakarta.validation.Valid;
77
import lombok.RequiredArgsConstructor;
8+
import org.ject.support.admin.account.dto.AdminAccountResponse;
89
import org.ject.support.admin.account.dto.AdminAccountActiveUpdateRequest;
910
import org.ject.support.admin.account.dto.AdminAccountCreateRequest;
1011
import org.ject.support.admin.account.dto.AdminAccountRoleUpdateRequest;
1112
import org.ject.support.admin.account.dto.AdminAccountUpdateRequest;
13+
import org.ject.support.admin.account.dto.AdminAccountSearchCondition;
1214
import org.ject.support.admin.account.service.AdminAccountService;
1315
import org.ject.support.common.security.AuthPrincipal;
16+
import org.ject.support.domain.member.MemberStatus;
17+
import org.ject.support.domain.member.Role;
18+
import org.springframework.data.domain.Page;
19+
import org.springframework.data.domain.Pageable;
20+
import org.springframework.data.domain.Sort.Direction;
21+
import org.springframework.data.web.PageableDefault;
1422
import org.springframework.security.access.prepost.PreAuthorize;
23+
import org.springframework.web.bind.annotation.GetMapping;
1524
import org.springframework.web.bind.annotation.PatchMapping;
1625
import org.springframework.web.bind.annotation.PathVariable;
1726
import org.springframework.web.bind.annotation.PostMapping;
1827
import org.springframework.web.bind.annotation.RequestBody;
1928
import org.springframework.web.bind.annotation.RequestMapping;
29+
import org.springframework.web.bind.annotation.RequestParam;
2030
import org.springframework.web.bind.annotation.RestController;
2131

32+
import java.util.List;
33+
2234
@RestController
2335
@RequiredArgsConstructor
2436
@RequestMapping("/admin/accounts")
@@ -27,6 +39,19 @@ public class AdminAccountController {
2739

2840
private final AdminAccountService adminAccountService;
2941

42+
@GetMapping
43+
@PreAuthorize("hasAuthority('ROLE_ADMIN')")
44+
@Operation(
45+
summary = "관리자 계정 목록 조회",
46+
description = "관리자 계정 목록을 조회합니다. 계정 유형과 상태를 복수 필터로 선택할 수 있습니다.")
47+
public Page<AdminAccountResponse> findAccounts(
48+
@RequestParam(required = false) final List<Role> roles,
49+
@RequestParam(required = false) final List<MemberStatus> statuses,
50+
@PageableDefault(sort = "createdAt", direction = Direction.DESC) final Pageable pageable) {
51+
AdminAccountSearchCondition condition = new AdminAccountSearchCondition(roles, statuses);
52+
return adminAccountService.findAccounts(condition, pageable);
53+
}
54+
3055
@PostMapping
3156
@PreAuthorize("hasAuthority('ROLE_ADMIN')")
3257
@Operation(
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
package org.ject.support.admin.account.dto;
2+
3+
import org.ject.support.domain.member.MemberStatus;
4+
import org.ject.support.domain.member.Role;
5+
import org.ject.support.domain.member.dto.MemberAccountProjection;
6+
7+
public record AdminAccountResponse(
8+
Long id,
9+
String email,
10+
String name,
11+
Role role,
12+
MemberStatus status
13+
) {
14+
15+
public static AdminAccountResponse from(final MemberAccountProjection projection) {
16+
return new AdminAccountResponse(
17+
projection.id(),
18+
projection.email(),
19+
projection.name(),
20+
projection.role(),
21+
projection.status());
22+
}
23+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
package org.ject.support.admin.account.dto;
2+
3+
import org.ject.support.domain.member.MemberStatus;
4+
import org.ject.support.domain.member.Role;
5+
6+
import java.util.List;
7+
8+
public record AdminAccountSearchCondition(
9+
List<Role> roles,
10+
List<MemberStatus> statuses
11+
) {
12+
}

src/main/java/org/ject/support/admin/account/service/AdminAccountService.java

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@
44
import org.ject.support.admin.account.dto.AdminAccountActiveUpdateRequest;
55
import org.ject.support.admin.account.dto.AdminAccountCreateRequest;
66
import org.ject.support.admin.account.dto.AdminAccountRoleUpdateRequest;
7+
import org.ject.support.admin.account.dto.AdminAccountResponse;
78
import org.ject.support.admin.account.dto.AdminAccountUpdateRequest;
9+
import org.ject.support.admin.account.dto.AdminAccountSearchCondition;
810
import org.ject.support.admin.component.AdminMemberComponent;
911
import org.ject.support.admin.exception.AdminErrorCode;
1012
import org.ject.support.admin.exception.AdminException;
@@ -13,10 +15,14 @@
1315
import org.ject.support.domain.member.entity.Member;
1416
import org.ject.support.domain.member.entity.MemberEditor;
1517
import org.ject.support.domain.member.repository.MemberRepository;
18+
import org.springframework.data.domain.Page;
19+
import org.springframework.data.domain.Pageable;
1620
import org.springframework.security.crypto.password.PasswordEncoder;
1721
import org.springframework.stereotype.Service;
1822
import org.springframework.transaction.annotation.Transactional;
1923

24+
import java.util.List;
25+
2026
@Service
2127
@RequiredArgsConstructor
2228
@Transactional(readOnly = true)
@@ -26,6 +32,13 @@ public class AdminAccountService {
2632
private final AdminMemberComponent adminMemberComponent;
2733
private final PasswordEncoder passwordEncoder;
2834

35+
public Page<AdminAccountResponse> findAccounts(final AdminAccountSearchCondition condition,
36+
final Pageable pageable) {
37+
validateBackofficeRoles(condition.roles());
38+
return memberRepository.findAccounts(condition, pageable)
39+
.map(AdminAccountResponse::from);
40+
}
41+
2942
@Transactional
3043
public void createAccount(final AdminAccountCreateRequest request) {
3144
validateBackofficeRole(request.role());
@@ -102,6 +115,16 @@ private void validateBackofficeRole(final Role role) {
102115
}
103116
}
104117

118+
private void validateBackofficeRoles(final List<Role> roles) {
119+
if (roles == null || roles.isEmpty()) {
120+
return;
121+
}
122+
123+
if (roles.stream().anyMatch(role -> !role.isBackoffice())) {
124+
throw new AdminException(AdminErrorCode.INVALID_ADMIN_ACCOUNT_ROLE);
125+
}
126+
}
127+
105128
private void validateEmailUniqueness(final AdminAccountCreateRequest request) {
106129
if (memberRepository.existsByEmail(request.email())) {
107130
throw new AdminException(AdminErrorCode.DUPLICATE_ADMIN_EMAIL);
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package org.ject.support.domain.member.dto;
2+
3+
import org.ject.support.domain.member.MemberStatus;
4+
import org.ject.support.domain.member.Role;
5+
6+
public record MemberAccountProjection(
7+
Long id,
8+
String email,
9+
String name,
10+
Role role,
11+
MemberStatus status
12+
) {
13+
}
Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,18 @@
11
package org.ject.support.domain.member.repository;
22

3-
import java.util.List;
3+
import org.ject.support.admin.account.dto.AdminAccountSearchCondition;
4+
import org.ject.support.domain.member.dto.MemberAccountProjection;
45
import org.ject.support.domain.member.dto.TeamMemberNames;
6+
import org.springframework.data.domain.Page;
7+
import org.springframework.data.domain.Pageable;
8+
9+
import java.util.List;
510

611
public interface MemberQueryRepository {
712

813
TeamMemberNames findMemberNamesByTeamId(Long teamId);
914

1015
List<String> findEmailsByIdsAndNotSubmitted(List<Long> applicantIds);
16+
17+
Page<MemberAccountProjection> findAccounts(AdminAccountSearchCondition condition, Pageable pageable);
1118
}

src/main/java/org/ject/support/domain/member/repository/MemberQueryRepositoryImpl.java

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,22 @@
1010
import static org.ject.support.domain.member.entity.QTeamMember.teamMember;
1111

1212
import com.querydsl.core.group.GroupBy;
13+
import com.querydsl.core.types.Projections;
1314
import com.querydsl.core.types.dsl.CaseBuilder;
15+
import com.querydsl.core.types.dsl.BooleanExpression;
1416
import com.querydsl.jpa.JPQLQueryFactory;
1517
import java.util.List;
18+
import java.util.Optional;
1619
import lombok.RequiredArgsConstructor;
20+
import org.ject.support.admin.account.dto.AdminAccountSearchCondition;
21+
import org.ject.support.common.data.PageResponse;
22+
import org.ject.support.domain.member.MemberStatus;
23+
import org.ject.support.domain.member.Role;
24+
import org.ject.support.domain.member.dto.MemberAccountProjection;
1725
import org.ject.support.domain.member.dto.QTeamMemberNames;
1826
import org.ject.support.domain.member.dto.TeamMemberNames;
27+
import org.springframework.data.domain.Page;
28+
import org.springframework.data.domain.Pageable;
1929
import org.springframework.stereotype.Repository;
2030

2131
@Repository
@@ -65,4 +75,55 @@ public List<String> findEmailsByIdsAndNotSubmitted(List<Long> applicantIds) {
6575
apply.status.eq(SUBMITTED).not())
6676
.fetch();
6777
}
78+
79+
@Override
80+
public Page<MemberAccountProjection> findAccounts(final AdminAccountSearchCondition condition,
81+
final Pageable pageable) {
82+
List<MemberAccountProjection> content = queryFactory
83+
.select(Projections.constructor(
84+
MemberAccountProjection.class,
85+
member.id,
86+
member.email,
87+
member.name,
88+
member.role,
89+
member.status))
90+
.from(member)
91+
.where(
92+
member.isDeleted.eq(false),
93+
member.role.in(Role.backofficeRoles()),
94+
inRoles(condition.roles()),
95+
inStatuses(condition.statuses())
96+
)
97+
.orderBy(member.createdAt.desc(), member.id.desc())
98+
.offset(pageable.getOffset())
99+
.limit(pageable.getPageSize())
100+
.fetch();
101+
102+
Long total = queryFactory
103+
.select(member.count())
104+
.from(member)
105+
.where(
106+
member.isDeleted.eq(false),
107+
member.role.in(Role.backofficeRoles()),
108+
inRoles(condition.roles()),
109+
inStatuses(condition.statuses())
110+
)
111+
.fetchOne();
112+
113+
return PageResponse.from(content, pageable, total != null ? total : 0L);
114+
}
115+
116+
private BooleanExpression inRoles(final List<Role> roles) {
117+
return Optional.ofNullable(roles)
118+
.filter(values -> !values.isEmpty())
119+
.map(member.role::in)
120+
.orElse(null);
121+
}
122+
123+
private BooleanExpression inStatuses(final List<MemberStatus> statuses) {
124+
return Optional.ofNullable(statuses)
125+
.filter(values -> !values.isEmpty())
126+
.map(member.status::in)
127+
.orElse(null);
128+
}
68129
}

src/test/java/org/ject/support/admin/account/controller/AdminAccountControllerTest.java

Lines changed: 74 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,31 +4,42 @@
44
import org.ject.support.admin.account.dto.AdminAccountActiveUpdateRequest;
55
import org.ject.support.admin.account.dto.AdminAccountCreateRequest;
66
import org.ject.support.admin.account.dto.AdminAccountRoleUpdateRequest;
7+
import org.ject.support.admin.account.dto.AdminAccountResponse;
78
import org.ject.support.admin.account.dto.AdminAccountUpdateRequest;
89
import org.ject.support.admin.account.service.AdminAccountService;
910
import org.ject.support.base.UnitTestSupport;
1011
import org.ject.support.common.exception.GlobalErrorCode;
1112
import org.ject.support.common.exception.GlobalExceptionHandler;
1213
import org.ject.support.common.security.AuthenticatedMemberIdResolver;
1314
import org.ject.support.common.security.CustomUserDetails;
15+
import org.ject.support.domain.member.MemberStatus;
1416
import org.ject.support.domain.member.Role;
1517
import org.junit.jupiter.api.AfterEach;
1618
import org.junit.jupiter.api.BeforeEach;
1719
import org.junit.jupiter.api.Test;
1820
import org.mockito.InjectMocks;
1921
import org.mockito.Mock;
22+
import org.springframework.data.domain.PageImpl;
23+
import org.springframework.data.domain.PageRequest;
24+
import org.springframework.data.domain.Pageable;
25+
import org.springframework.data.web.PageableHandlerMethodArgumentResolver;
2026
import org.springframework.http.MediaType;
2127
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
2228
import org.springframework.security.access.prepost.PreAuthorize;
2329
import org.springframework.security.core.context.SecurityContextHolder;
2430
import org.springframework.test.web.servlet.MockMvc;
2531
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
2632

33+
import java.util.List;
34+
2735
import static org.assertj.core.api.Assertions.assertThat;
2836
import static org.mockito.ArgumentMatchers.any;
37+
import static org.mockito.ArgumentMatchers.argThat;
2938
import static org.mockito.ArgumentMatchers.eq;
39+
import static org.mockito.BDDMockito.given;
3040
import static org.mockito.Mockito.doNothing;
3141
import static org.mockito.Mockito.verify;
42+
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
3243
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.patch;
3344
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
3445
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
@@ -51,7 +62,9 @@ void setUp() {
5162
objectMapper = new ObjectMapper();
5263
mockMvc = MockMvcBuilders.standaloneSetup(adminAccountController)
5364
.setControllerAdvice(new GlobalExceptionHandler())
54-
.setCustomArgumentResolvers(new AuthenticatedMemberIdResolver())
65+
.setCustomArgumentResolvers(
66+
new AuthenticatedMemberIdResolver(),
67+
new PageableHandlerMethodArgumentResolver())
5568
.build();
5669
}
5770

@@ -60,6 +73,66 @@ void tearDown() {
6073
SecurityContextHolder.clearContext();
6174
}
6275

76+
@Test
77+
void 관리자_계정_목록_조회_성공() throws Exception {
78+
// given
79+
var response = new AdminAccountResponse(1L, "admin@ject.kr", "김젝트", Role.ADMIN, MemberStatus.ACTIVE);
80+
var page = new PageImpl<>(List.of(response), PageRequest.of(0, 20), 1);
81+
82+
given(adminAccountService.findAccounts(any(), any(Pageable.class))).willReturn(page);
83+
84+
// when, then
85+
mockMvc.perform(get("/admin/accounts")
86+
.param("roles", "ADMIN", "SUPPORTER")
87+
.param("statuses", "ACTIVE", "LOCKED")
88+
.param("page", "0")
89+
.param("size", "20"))
90+
.andExpect(status().isOk())
91+
.andExpect(jsonPath("$.content[0].id").value(1))
92+
.andExpect(jsonPath("$.content[0].email").value("admin@ject.kr"))
93+
.andExpect(jsonPath("$.content[0].name").value("김젝트"))
94+
.andExpect(jsonPath("$.content[0].role").value("ADMIN"))
95+
.andExpect(jsonPath("$.content[0].status").value("ACTIVE"))
96+
.andDo(print());
97+
98+
verify(adminAccountService).findAccounts(
99+
argThat(condition ->
100+
condition.roles().equals(List.of(Role.ADMIN, Role.SUPPORTER))
101+
&& condition.statuses().equals(List.of(MemberStatus.ACTIVE, MemberStatus.LOCKED))),
102+
any(Pageable.class));
103+
}
104+
105+
@Test
106+
void 관리자_계정_목록_조회는_필터를_생략할_수_있다() throws Exception {
107+
// given
108+
var page = new PageImpl<AdminAccountResponse>(List.of(), PageRequest.of(0, 20), 0);
109+
110+
given(adminAccountService.findAccounts(any(), any(Pageable.class))).willReturn(page);
111+
112+
// when, then
113+
mockMvc.perform(get("/admin/accounts")
114+
.param("page", "0")
115+
.param("size", "20"))
116+
.andExpect(status().isOk())
117+
.andDo(print());
118+
119+
verify(adminAccountService).findAccounts(
120+
argThat(condition -> condition.roles() == null && condition.statuses() == null),
121+
any(Pageable.class));
122+
}
123+
124+
@Test
125+
void 관리자_계정_목록_조회는_ADMIN_권한만_허용한다() throws Exception {
126+
// when
127+
PreAuthorize preAuthorize = AdminAccountController.class
128+
.getMethod("findAccounts", List.class, List.class, Pageable.class)
129+
.getAnnotation(PreAuthorize.class);
130+
131+
// then
132+
assertThat(preAuthorize).isNotNull();
133+
assertThat(preAuthorize.value()).isEqualTo("hasAuthority('ROLE_ADMIN')");
134+
}
135+
63136
@Test
64137
void 관리자_계정_생성_성공() throws Exception {
65138
// given

0 commit comments

Comments
 (0)