Skip to content

Commit b37593c

Browse files
authored
Merge pull request #315 from DevKor-github/temp
feat: 어드민 유저 검색, 유저 목록 추가
2 parents aae9157 + 227b9cd commit b37593c

5 files changed

Lines changed: 246 additions & 0 deletions

File tree

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
package org.devkor.apu.saerok_server.domain.admin.user.api;
2+
3+
import io.swagger.v3.oas.annotations.Operation;
4+
import io.swagger.v3.oas.annotations.media.Content;
5+
import io.swagger.v3.oas.annotations.media.Schema;
6+
import io.swagger.v3.oas.annotations.responses.ApiResponse;
7+
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
8+
import io.swagger.v3.oas.annotations.tags.Tag;
9+
import lombok.RequiredArgsConstructor;
10+
import org.devkor.apu.saerok_server.domain.admin.user.api.dto.response.AdminUserListResponse;
11+
import org.devkor.apu.saerok_server.domain.admin.user.application.AdminUserQueryService;
12+
import org.devkor.apu.saerok_server.global.shared.exception.BadRequestException;
13+
import org.springframework.security.access.prepost.PreAuthorize;
14+
import org.springframework.web.bind.annotation.GetMapping;
15+
import org.springframework.web.bind.annotation.RequestMapping;
16+
import org.springframework.web.bind.annotation.RequestParam;
17+
import org.springframework.web.bind.annotation.RestController;
18+
19+
@Tag(name = "Admin User API", description = "관리자 사용자 조회 API")
20+
@RestController
21+
@RequiredArgsConstructor
22+
@RequestMapping("${api_prefix}/admin/users")
23+
public class AdminUserController {
24+
25+
private static final int MAX_PAGE_SIZE = 50;
26+
27+
private final AdminUserQueryService queryService;
28+
29+
@GetMapping
30+
@PreAuthorize("@perm.has('ADMIN_ANNOUNCEMENT_WRITE')")
31+
@Operation(
32+
summary = "사용자 ID/닉네임 목록 조회",
33+
description = "대상 공지 발송용 활성 사용자 ID와 닉네임 목록을 조회합니다.",
34+
security = @SecurityRequirement(name = "bearerAuth"),
35+
responses = {
36+
@ApiResponse(
37+
responseCode = "200",
38+
description = "조회 성공",
39+
content = @Content(schema = @Schema(implementation = AdminUserListResponse.class))
40+
)
41+
}
42+
)
43+
public AdminUserListResponse listUsers(
44+
@RequestParam(required = false) String q,
45+
@RequestParam(defaultValue = "1") int page,
46+
@RequestParam(defaultValue = "20") int size
47+
) {
48+
validatePagination(page, size);
49+
return queryService.listUsers(q, page, size);
50+
}
51+
52+
private void validatePagination(int page, int size) {
53+
if (page < 1) {
54+
throw new BadRequestException("page는 1 이상의 숫자로 입력해 주세요.");
55+
}
56+
if (size < 1 || size > MAX_PAGE_SIZE) {
57+
throw new BadRequestException("size는 1 이상 50 이하의 숫자로 입력해 주세요.");
58+
}
59+
}
60+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
package org.devkor.apu.saerok_server.domain.admin.user.api.dto.response;
2+
3+
import io.swagger.v3.oas.annotations.media.Schema;
4+
5+
import java.util.List;
6+
7+
@Schema(description = "관리자 사용자 목록 응답")
8+
public record AdminUserListResponse(
9+
@Schema(description = "사용자 목록")
10+
List<Item> users,
11+
12+
@Schema(description = "현재 페이지", example = "1")
13+
int page,
14+
15+
@Schema(description = "페이지 크기", example = "20")
16+
int size,
17+
18+
@Schema(description = "전체 사용자 수", example = "120")
19+
long totalElements,
20+
21+
@Schema(description = "전체 페이지 수", example = "6")
22+
int totalPages
23+
) {
24+
25+
public record Item(
26+
@Schema(description = "사용자 ID", example = "501")
27+
Long id,
28+
29+
@Schema(description = "닉네임", example = "솔바람")
30+
String nickname
31+
) {
32+
}
33+
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
package org.devkor.apu.saerok_server.domain.admin.user.application;
2+
3+
import lombok.RequiredArgsConstructor;
4+
import org.devkor.apu.saerok_server.domain.admin.user.api.dto.response.AdminUserListResponse;
5+
import org.devkor.apu.saerok_server.domain.user.core.entity.User;
6+
import org.devkor.apu.saerok_server.domain.user.core.repository.UserRepository;
7+
import org.springframework.stereotype.Service;
8+
import org.springframework.transaction.annotation.Transactional;
9+
10+
import java.util.List;
11+
12+
@Service
13+
@Transactional(readOnly = true)
14+
@RequiredArgsConstructor
15+
public class AdminUserQueryService {
16+
17+
private final UserRepository userRepository;
18+
19+
public AdminUserListResponse listUsers(String query, int page, int size) {
20+
String normalizedQuery = normalizeQuery(query);
21+
int offset = (page - 1) * size;
22+
23+
List<User> users = userRepository.findActiveNicknameUsers(normalizedQuery, offset, size);
24+
long totalElements = userRepository.countActiveNicknameUsers(normalizedQuery);
25+
int totalPages = totalElements == 0 ? 0 : (int) Math.ceil((double) totalElements / size);
26+
27+
List<AdminUserListResponse.Item> items = users.stream()
28+
.map(user -> new AdminUserListResponse.Item(user.getId(), user.getNickname()))
29+
.toList();
30+
31+
return new AdminUserListResponse(items, page, size, totalElements, totalPages);
32+
}
33+
34+
private String normalizeQuery(String query) {
35+
if (query == null || query.isBlank()) {
36+
return null;
37+
}
38+
return query.trim();
39+
}
40+
}

src/main/java/org/devkor/apu/saerok_server/domain/user/core/repository/UserRepository.java

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,53 @@ public List<Long> findActiveUserIds(int offset, int limit) {
6262
.getResultList();
6363
}
6464

65+
public List<User> findActiveNicknameUsers(String nicknameQuery, int offset, int limit) {
66+
String jpql = """
67+
SELECT u FROM User u
68+
WHERE u.deletedAt IS NULL
69+
AND u.signupStatus <> :withdrawn
70+
AND u.nickname IS NOT NULL
71+
AND TRIM(u.nickname) <> ''
72+
""";
73+
if (nicknameQuery != null) {
74+
jpql += " AND u.nickname LIKE :nicknameQuery";
75+
}
76+
jpql += " ORDER BY u.nickname ASC, u.id ASC";
77+
78+
var query = em.createQuery(jpql, User.class)
79+
.setParameter("withdrawn", SignupStatusType.WITHDRAWN)
80+
.setFirstResult(offset)
81+
.setMaxResults(limit);
82+
83+
if (nicknameQuery != null) {
84+
query.setParameter("nicknameQuery", "%" + nicknameQuery + "%");
85+
}
86+
87+
return query.getResultList();
88+
}
89+
90+
public long countActiveNicknameUsers(String nicknameQuery) {
91+
String jpql = """
92+
SELECT COUNT(u) FROM User u
93+
WHERE u.deletedAt IS NULL
94+
AND u.signupStatus <> :withdrawn
95+
AND u.nickname IS NOT NULL
96+
AND TRIM(u.nickname) <> ''
97+
""";
98+
if (nicknameQuery != null) {
99+
jpql += " AND u.nickname LIKE :nicknameQuery";
100+
}
101+
102+
var query = em.createQuery(jpql, Long.class)
103+
.setParameter("withdrawn", SignupStatusType.WITHDRAWN);
104+
105+
if (nicknameQuery != null) {
106+
query.setParameter("nicknameQuery", "%" + nicknameQuery + "%");
107+
}
108+
109+
return query.getSingleResult();
110+
}
111+
65112
public List<User> findByIds(List<Long> ids) {
66113
if (ids == null || ids.isEmpty()) return List.of();
67114
return em.createQuery(

src/test/java/org/devkor/apu/saerok_server/domain/user/core/repository/UserRepositoryTest.java

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,72 @@ void findActiveUserIds_empty() {
138138
assertThat(activeIds).isEmpty();
139139
}
140140

141+
@Test @DisplayName("findActiveNicknameUsers - 활성 닉네임 사용자만 닉네임순으로 조회")
142+
void findActiveNicknameUsers() {
143+
User charlie = user("charlie@example.com", "charlie");
144+
charlie.setSignupStatus(SignupStatusType.COMPLETED);
145+
User bravo = user("bravo@example.com", "bravo");
146+
bravo.setSignupStatus(SignupStatusType.COMPLETED);
147+
User withdrawn = user("withdrawn-nickname@example.com", "alpha");
148+
withdrawn.setSignupStatus(SignupStatusType.WITHDRAWN);
149+
User deleted = user("deleted-nickname@example.com", "beta");
150+
User noNickname = user("no-nickname@example.com", null);
151+
noNickname.setSignupStatus(SignupStatusType.COMPLETED);
152+
User blankNickname = user("blank-nickname@example.com", "");
153+
blankNickname.setSignupStatus(SignupStatusType.COMPLETED);
154+
em.flush();
155+
156+
deleted.softDelete();
157+
em.flush(); em.clear();
158+
159+
List<User> users = repo.findActiveNicknameUsers(null, 0, 20);
160+
161+
assertThat(users)
162+
.extracting(User::getNickname)
163+
.containsExactly("bravo", "charlie");
164+
assertThat(repo.countActiveNicknameUsers(null)).isEqualTo(2);
165+
}
166+
167+
@Test @DisplayName("findActiveNicknameUsers - 닉네임 검색과 페이징")
168+
void findActiveNicknameUsers_queryAndPagination() {
169+
User alpha = user("alpha@example.com", "alpha");
170+
alpha.setSignupStatus(SignupStatusType.COMPLETED);
171+
User alpine = user("alpine@example.com", "alpine");
172+
alpine.setSignupStatus(SignupStatusType.COMPLETED);
173+
User bravo = user("bravo-query@example.com", "bravo");
174+
bravo.setSignupStatus(SignupStatusType.COMPLETED);
175+
em.flush(); em.clear();
176+
177+
List<User> firstPage = repo.findActiveNicknameUsers("alp", 0, 1);
178+
List<User> secondPage = repo.findActiveNicknameUsers("alp", 1, 1);
179+
180+
assertThat(firstPage)
181+
.extracting(User::getNickname)
182+
.containsExactly("alpha");
183+
assertThat(secondPage)
184+
.extracting(User::getNickname)
185+
.containsExactly("alpine");
186+
assertThat(repo.countActiveNicknameUsers("alp")).isEqualTo(2);
187+
}
188+
189+
@Test @DisplayName("findActiveNicknameUsers - 닉네임 중간 문자열 검색")
190+
void findActiveNicknameUsers_containsQuery() {
191+
User duli = user("duli@example.com", "둘리");
192+
duli.setSignupStatus(SignupStatusType.COMPLETED);
193+
User pigeon = user("pigeon@example.com", "비둘기");
194+
pigeon.setSignupStatus(SignupStatusType.COMPLETED);
195+
User magpie = user("magpie@example.com", "까치");
196+
magpie.setSignupStatus(SignupStatusType.COMPLETED);
197+
em.flush(); em.clear();
198+
199+
List<User> users = repo.findActiveNicknameUsers("둘", 0, 20);
200+
201+
assertThat(users)
202+
.extracting(User::getNickname)
203+
.containsExactly("둘리", "비둘기");
204+
assertThat(repo.countActiveNicknameUsers("둘")).isEqualTo(2);
205+
}
206+
141207
@Test @DisplayName("save - 중복 닉네임은 제약조건 위반")
142208
void save_duplicateNickname() {
143209
User u1 = user("user1@example.com");

0 commit comments

Comments
 (0)