Skip to content

Commit d124043

Browse files
committed
✨ feat: 전체 유저 목록 조회 기능
1 parent 30f071c commit d124043

File tree

8 files changed

+177
-3
lines changed

8 files changed

+177
-3
lines changed
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
package io.f1.backend.domain.admin.api;
2+
3+
import io.f1.backend.domain.admin.app.AdminService;
4+
import io.f1.backend.domain.admin.dto.UserPageResponse;
5+
import io.f1.backend.global.validation.LimitPageSize;
6+
import lombok.RequiredArgsConstructor;
7+
import org.springframework.data.domain.Pageable;
8+
import org.springframework.data.domain.Sort.Direction;
9+
import org.springframework.data.web.PageableDefault;
10+
import org.springframework.http.ResponseEntity;
11+
import org.springframework.web.bind.annotation.GetMapping;
12+
import org.springframework.web.bind.annotation.RequestMapping;
13+
import org.springframework.web.bind.annotation.RestController;
14+
15+
@RestController
16+
@RequestMapping("/admin")
17+
@RequiredArgsConstructor
18+
public class AdminController {
19+
20+
private final AdminService adminService;
21+
22+
@GetMapping("/users")
23+
@LimitPageSize
24+
public ResponseEntity<UserPageResponse> getUsers(
25+
@PageableDefault(sort = "lastLogin", direction = Direction.DESC) Pageable pageable
26+
) {
27+
UserPageResponse response = adminService.getAllUsers(pageable);
28+
return ResponseEntity.ok().body(response);
29+
}
30+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
package io.f1.backend.domain.admin.app;
2+
3+
import static io.f1.backend.domain.admin.mapper.AdminMapper.toUserListPageResponse;
4+
5+
import io.f1.backend.domain.admin.dto.UserPageResponse;
6+
import io.f1.backend.domain.admin.dto.UserResponse;
7+
import io.f1.backend.domain.user.dao.UserRepository;
8+
import lombok.RequiredArgsConstructor;
9+
import org.springframework.data.domain.Page;
10+
import org.springframework.data.domain.Pageable;
11+
import org.springframework.stereotype.Service;
12+
import org.springframework.transaction.annotation.Transactional;
13+
14+
@Service
15+
@RequiredArgsConstructor
16+
public class AdminService {
17+
18+
private final UserRepository userRepository;
19+
20+
@Transactional(readOnly = true)
21+
public UserPageResponse getAllUsers(Pageable pageable) {
22+
Page<UserResponse> users = userRepository.findAllUsersWithPaging(pageable);
23+
return toUserListPageResponse(users);
24+
}
25+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
package io.f1.backend.domain.admin.dto;
2+
3+
import java.util.List;
4+
5+
public record UserPageResponse(int totalPages, int currentPage, int totalElements, List<UserResponse> users) {
6+
7+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
package io.f1.backend.domain.admin.dto;
2+
3+
import java.time.LocalDateTime;
4+
5+
public record UserResponse(Long id, String nickname, LocalDateTime lastLogin,
6+
LocalDateTime createdAt) {
7+
8+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
package io.f1.backend.domain.admin.mapper;
2+
3+
import io.f1.backend.domain.admin.dto.UserPageResponse;
4+
import io.f1.backend.domain.admin.dto.UserResponse;
5+
import org.springframework.data.domain.Page;
6+
7+
public class AdminMapper {
8+
9+
private AdminMapper() {
10+
}
11+
12+
public static UserPageResponse toUserListPageResponse(Page<UserResponse> userPage) {
13+
int curPage = userPage.getNumber() + 1;
14+
15+
return new UserPageResponse(
16+
userPage.getTotalPages(),
17+
curPage,
18+
userPage.getNumberOfElements(),
19+
userPage.getContent()
20+
);
21+
}
22+
}
Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,25 @@
11
package io.f1.backend.domain.user.dao;
22

3+
import io.f1.backend.domain.admin.dto.UserResponse;
34
import io.f1.backend.domain.user.entity.User;
4-
5+
import java.util.Optional;
6+
import org.springframework.data.domain.Page;
7+
import org.springframework.data.domain.Pageable;
58
import org.springframework.data.jpa.repository.JpaRepository;
9+
import org.springframework.data.jpa.repository.Query;
610
import org.springframework.stereotype.Repository;
711

8-
import java.util.Optional;
9-
1012
@Repository
1113
public interface UserRepository extends JpaRepository<User, Long> {
1214

1315
Optional<User> findByProviderAndProviderId(String provider, String providerId);
1416

1517
Boolean existsUserByNickname(String nickname);
18+
19+
@Query(
20+
"SELECT new io.f1.backend.domain.admin.dto.UserResponse(u.id, u.nickname, u.lastLogin, u.createdAt)" +
21+
"FROM User u " +
22+
"ORDER BY u.lastLogin DESC"
23+
)
24+
Page<UserResponse> findAllUsersWithPaging(Pageable pageable);
1625
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
package io.f1.backend.domain.admin.app;
2+
3+
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
4+
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
5+
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
6+
7+
import com.github.database.rider.core.api.dataset.DataSet;
8+
import io.f1.backend.global.template.BrowserTestTemplate;
9+
import org.junit.jupiter.api.DisplayName;
10+
import org.junit.jupiter.api.Test;
11+
import org.springframework.security.test.context.support.WithMockUser;
12+
import org.springframework.test.web.servlet.ResultActions;
13+
14+
@WithMockUser(roles = "ADMIN")
15+
public class AdminServiceTests extends BrowserTestTemplate {
16+
17+
@Test
18+
@DataSet("datasets/admin/sorted-user.yml")
19+
@DisplayName("가입한 유저가 3명이면 첫 페이지에서 3개의 결과를 반환한다")
20+
void totalUser() throws Exception {
21+
// when
22+
ResultActions result = mockMvc.perform(get("/admin/users"));
23+
24+
// then
25+
result.andExpectAll(
26+
status().isOk(),
27+
jsonPath("$.totalPages").value(1),
28+
jsonPath("$.currentPage").value(1),
29+
jsonPath("$.totalElements").value(3),
30+
jsonPath("$.users.length()").value(3));
31+
}
32+
33+
@Test
34+
@DataSet("datasets/admin/sorted-user.yml")
35+
@DisplayName("유저 목록이 최근 로그인 순(내림차순)으로 정렬되어 반환된다")
36+
void getUsersSortedByLastLogin() throws Exception {
37+
// when
38+
ResultActions result = mockMvc.perform(get("/admin/users"));
39+
// then
40+
result.andExpectAll(
41+
status().isOk(),
42+
jsonPath("$.totalElements").value(3),
43+
// 가장 최근 로그인한 USER3이 첫 번째
44+
jsonPath("$.users[0].nickname").value("USER3"),
45+
// 중간 로그인한 USER2가 두 번째
46+
jsonPath("$.users[1].nickname").value("USER2"),
47+
// 가장 오래된 로그인한 USER1이 세 번째
48+
jsonPath("$.users[2].nickname").value("USER1")
49+
);
50+
}
51+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
2+
user_test:
3+
- id: 1
4+
nickname: "USER1"
5+
provider: "kakao"
6+
provider_id: "kakao1"
7+
last_login: 2025-07-16 09:00:00 # 가장 오래된 로그인
8+
created_at: 2025-07-01 10:00:00
9+
10+
- id: 2
11+
nickname: "USER2"
12+
provider: "kakao"
13+
provider_id: "kakao2"
14+
last_login: 2025-07-17 12:00:00 # 중간 로그인
15+
created_at: 2025-07-02 10:00:00
16+
17+
- id: 3
18+
nickname: "USER3"
19+
provider: "kakao"
20+
provider_id: "kakao3"
21+
last_login: 2025-07-18 15:00:00 # 가장 최근 로그인
22+
created_at: 2025-07-03 10:00:00

0 commit comments

Comments
 (0)