Skip to content

Commit cfb8140

Browse files
authored
Merge pull request #303 from prgrms-web-devcourse-final-project/develop
Feat/249 (#302)
2 parents e3f8c37 + fcc3834 commit cfb8140

File tree

7 files changed

+294
-0
lines changed

7 files changed

+294
-0
lines changed

back/src/main/java/com/back/domain/member/member/controller/AdmMemberController.java

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package com.back.domain.member.member.controller;
22

3+
import com.back.domain.member.member.dto.MemberPagingResponse;
34
import com.back.domain.member.member.dto.MemberSearchResponse;
45
import com.back.domain.member.member.dto.MentorUpdateRequest;
56
import com.back.domain.member.member.dto.MenteeUpdateRequest;
@@ -21,6 +22,16 @@ public class AdmMemberController {
2122
private final Rq rq;
2223

2324

25+
@GetMapping
26+
@Operation(summary = "회원 목록 조회 (관리자) - 페이징, 페이지는 기본으로 10개씩 조회")
27+
@PreAuthorize("hasRole('ADMIN')")
28+
public RsData<MemberPagingResponse> getAllMembers(
29+
@RequestParam(defaultValue = "0") int page,
30+
@RequestParam(defaultValue = "10") int size) {
31+
MemberPagingResponse members = memberService.getAllMembersForAdmin(page, size);
32+
return new RsData<>("200-18", "회원 목록 조회 성공", members);
33+
}
34+
2435
@GetMapping("/{memberId}")
2536
@Operation(summary = "회원 상세 조회 (관리자)")
2637
@PreAuthorize("hasRole('ADMIN')")
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
package com.back.domain.member.member.dto;
2+
3+
import com.back.domain.member.member.entity.Member;
4+
5+
import java.time.LocalDateTime;
6+
7+
public record MemberListResponse(
8+
Long id,
9+
String email,
10+
String name,
11+
String nickname,
12+
Member.Role role,
13+
Boolean isDeleted,
14+
LocalDateTime createdAt
15+
) {
16+
public static MemberListResponse from(Member member) {
17+
return new MemberListResponse(
18+
member.getId(),
19+
member.getEmail(),
20+
member.getName(),
21+
member.getNickname(),
22+
member.getRole(),
23+
member.getIsDeleted(),
24+
member.getCreateDate()
25+
);
26+
}
27+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
package com.back.domain.member.member.dto;
2+
3+
import io.swagger.v3.oas.annotations.media.Schema;
4+
import org.springframework.data.domain.Page;
5+
6+
import java.util.List;
7+
8+
public record MemberPagingResponse(
9+
@Schema(description = "회원 목록")
10+
List<MemberListResponse> members,
11+
@Schema(description = "현재 페이지 (0부터 시작)")
12+
int currentPage,
13+
@Schema(description = "총 페이지")
14+
int totalPage,
15+
@Schema(description = "총 개수")
16+
long totalElements,
17+
@Schema(description = "다음 페이지 존재 여부")
18+
boolean hasNext
19+
) {
20+
21+
public static MemberPagingResponse from(Page<MemberListResponse> page) {
22+
return new MemberPagingResponse(
23+
page.getContent(),
24+
page.getNumber(),
25+
page.getTotalPages(),
26+
page.getTotalElements(),
27+
page.hasNext()
28+
);
29+
}
30+
}

back/src/main/java/com/back/domain/member/member/repository/MemberRepository.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
package com.back.domain.member.member.repository;
22

33
import com.back.domain.member.member.entity.Member;
4+
import org.springframework.data.domain.Page;
5+
import org.springframework.data.domain.Pageable;
46
import org.springframework.data.jpa.repository.JpaRepository;
57
import org.springframework.data.jpa.repository.Query;
68
import org.springframework.data.repository.query.Param;
@@ -27,4 +29,7 @@ public interface MemberRepository extends JpaRepository<Member, Long> {
2729

2830
@Query("SELECT m FROM Member m WHERE m.id = :id")
2931
Optional<Member> findByIdIncludingDeleted(@Param("id") Long id);
32+
33+
@Query("SELECT m FROM Member m ORDER BY m.createDate DESC")
34+
Page<Member> findAllIncludingDeleted(Pageable pageable);
3035
}

back/src/main/java/com/back/domain/member/member/service/MemberService.java

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@
1111
import com.back.domain.member.mentor.repository.MentorRepository;
1212
import com.back.global.exception.ServiceException;
1313
import lombok.RequiredArgsConstructor;
14+
import org.springframework.data.domain.Page;
15+
import org.springframework.data.domain.PageRequest;
16+
import org.springframework.data.domain.Pageable;
1417
import org.springframework.security.crypto.password.PasswordEncoder;
1518
import org.springframework.stereotype.Service;
1619
import org.springframework.transaction.annotation.Transactional;
@@ -276,6 +279,14 @@ public void updateMentor(Member currentUser, MentorUpdateRequest request) {
276279
}
277280

278281

282+
public MemberPagingResponse getAllMembersForAdmin(int page, int size) {
283+
Pageable pageable = PageRequest.of(page, size);
284+
Page<Member> members = memberRepository.findAllIncludingDeleted(pageable);
285+
286+
Page<MemberListResponse> memberPage = members.map(MemberListResponse::from);
287+
return MemberPagingResponse.from(memberPage);
288+
}
289+
279290
public MemberSearchResponse getMemberForAdmin(Long memberId) {
280291
Member member = memberRepository.findByIdIncludingDeleted(memberId)
281292
.orElseThrow(() -> new ServiceException("404-1", "존재하지 않는 회원입니다."));
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
package com.back.global.initData;
2+
3+
import com.back.domain.member.member.entity.Member;
4+
import com.back.domain.member.member.repository.MemberRepository;
5+
import lombok.RequiredArgsConstructor;
6+
import lombok.extern.slf4j.Slf4j;
7+
import org.springframework.boot.ApplicationRunner;
8+
import org.springframework.context.annotation.Bean;
9+
import org.springframework.context.annotation.Profile;
10+
import org.springframework.security.crypto.password.PasswordEncoder;
11+
import org.springframework.stereotype.Component;
12+
13+
@Component
14+
@Profile("dev")
15+
@RequiredArgsConstructor
16+
@Slf4j
17+
public class AdminInitData {
18+
19+
private final MemberRepository memberRepository;
20+
private final PasswordEncoder passwordEncoder;
21+
22+
@Bean
23+
public ApplicationRunner initAdmin() {
24+
return args -> {
25+
if (memberRepository.findByEmail("[email protected]").isEmpty()) {
26+
Member admin = new Member(
27+
28+
passwordEncoder.encode("admin1234"),
29+
"관리자",
30+
"admin",
31+
Member.Role.ADMIN
32+
);
33+
memberRepository.save(admin);
34+
log.info("어드민 계정이 생성되었습니다. (email: [email protected], password: admin1234)");
35+
} else {
36+
log.info("어드민 계정이 이미 존재합니다.");
37+
}
38+
};
39+
}
40+
41+
}

back/src/test/java/com/back/domain/member/member/controller/AdminMemberControllerTest.java

Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -295,4 +295,173 @@ void t12() throws Exception {
295295

296296
result.andExpect(status().is4xxClientError());
297297
}
298+
299+
@Test
300+
@DisplayName("관리자 회원 목록 조회 성공 - 기본 페이징")
301+
@WithMockUser(roles = "ADMIN")
302+
void t13() throws Exception {
303+
// 테스트용 회원 3명 생성
304+
memberService.joinMentee("[email protected]", "멘티1", "멘티닉네임1", "password123", "Backend");
305+
memberService.joinMentor("[email protected]", "멘토1", "멘토닉네임1", "password123", "Frontend", 3);
306+
memberService.joinMentee("[email protected]", "멘티2", "멘티닉네임2", "password123", "AI/ML");
307+
308+
// 관리자가 회원 목록 조회 (기본값: page=0, size=10)
309+
ResultActions result = mvc
310+
.perform(get("/members"))
311+
.andDo(print());
312+
313+
result
314+
.andExpect(handler().handlerType(AdmMemberController.class))
315+
.andExpect(handler().methodName("getAllMembers"))
316+
.andExpect(status().isOk())
317+
.andExpect(jsonPath("$.resultCode").value("200-18"))
318+
.andExpect(jsonPath("$.msg").value("회원 목록 조회 성공"))
319+
.andExpect(jsonPath("$.data.members").isArray())
320+
.andExpect(jsonPath("$.data.totalElements").exists())
321+
.andExpect(jsonPath("$.data.totalPage").exists())
322+
.andExpect(jsonPath("$.data.currentPage").value(0))
323+
.andExpect(jsonPath("$.data.hasNext").exists());
324+
}
325+
326+
@Test
327+
@DisplayName("관리자 회원 목록 조회 성공 - 커스텀 페이징")
328+
@WithMockUser(roles = "ADMIN")
329+
void t14() throws Exception {
330+
// 테스트용 회원 5명 생성
331+
memberService.joinMentee("[email protected]", "유저1", "닉네임1", "password123", "Backend");
332+
memberService.joinMentee("[email protected]", "유저2", "닉네임2", "password123", "Frontend");
333+
memberService.joinMentor("[email protected]", "유저3", "닉네임3", "password123", "Backend", 2);
334+
memberService.joinMentor("[email protected]", "유저4", "닉네임4", "password123", "AI/ML", 5);
335+
memberService.joinMentee("[email protected]", "유저5", "닉네임5", "password123", "DevOps");
336+
337+
// 페이지 크기 2로 첫 페이지 조회
338+
ResultActions result = mvc
339+
.perform(get("/members")
340+
.param("page", "0")
341+
.param("size", "2"))
342+
.andDo(print());
343+
344+
result
345+
.andExpect(status().isOk())
346+
.andExpect(jsonPath("$.resultCode").value("200-18"))
347+
.andExpect(jsonPath("$.data.members").isArray())
348+
.andExpect(jsonPath("$.data.currentPage").value(0))
349+
.andExpect(jsonPath("$.data.members.length()").value(2));
350+
}
351+
352+
@Test
353+
@DisplayName("관리자 회원 목록 조회 - 두 번째 페이지")
354+
@WithMockUser(roles = "ADMIN")
355+
void t15() throws Exception {
356+
// 테스트용 회원 5명 생성
357+
memberService.joinMentee("[email protected]", "페이지유저1", "페이지닉네임1", "password123", "Backend");
358+
memberService.joinMentee("[email protected]", "페이지유저2", "페이지닉네임2", "password123", "Frontend");
359+
memberService.joinMentor("[email protected]", "페이지유저3", "페이지닉네임3", "password123", "Backend", 3);
360+
memberService.joinMentor("[email protected]", "페이지유저4", "페이지닉네임4", "password123", "AI/ML", 4);
361+
memberService.joinMentee("[email protected]", "페이지유저5", "페이지닉네임5", "password123", "DevOps");
362+
363+
// 페이지 크기 2로 두 번째 페이지 조회
364+
ResultActions result = mvc
365+
.perform(get("/members")
366+
.param("page", "1")
367+
.param("size", "2"))
368+
.andDo(print());
369+
370+
result
371+
.andExpect(status().isOk())
372+
.andExpect(jsonPath("$.resultCode").value("200-18"))
373+
.andExpect(jsonPath("$.data.members").isArray())
374+
.andExpect(jsonPath("$.data.currentPage").value(1));
375+
}
376+
377+
@Test
378+
@DisplayName("관리자 회원 목록 조회 - 기본 정보만 포함 확인")
379+
@WithMockUser(roles = "ADMIN")
380+
void t16() throws Exception {
381+
// 멘토와 멘티 생성
382+
memberService.joinMentor("[email protected]", "멘토유저", "멘토닉네임", "password123", "Backend", 5);
383+
memberService.joinMentee("[email protected]", "멘티유저", "멘티닉네임", "password123", "Frontend");
384+
385+
// 회원 목록 조회
386+
ResultActions result = mvc
387+
.perform(get("/members")
388+
.param("size", "10"))
389+
.andDo(print());
390+
391+
result
392+
.andExpect(status().isOk())
393+
.andExpect(jsonPath("$.data.members").isArray())
394+
// 기본 정보만 포함되어야 함
395+
.andExpect(jsonPath("$.data.members[0].id").exists())
396+
.andExpect(jsonPath("$.data.members[0].email").exists())
397+
.andExpect(jsonPath("$.data.members[0].name").exists())
398+
.andExpect(jsonPath("$.data.members[0].nickname").exists())
399+
.andExpect(jsonPath("$.data.members[0].role").exists())
400+
.andExpect(jsonPath("$.data.members[0].isDeleted").exists())
401+
.andExpect(jsonPath("$.data.members[0].createdAt").exists());
402+
}
403+
404+
@Test
405+
@DisplayName("관리자 회원 목록 조회 - 삭제된 회원도 포함")
406+
@WithMockUser(roles = "ADMIN")
407+
void t17() throws Exception {
408+
// 회원 생성 후 삭제
409+
Member member = memberService.joinMentee("[email protected]", "삭제될유저", "삭제될닉네임", "password123", "Backend");
410+
memberService.deleteMemberByAdmin(member.getId());
411+
412+
// 회원 목록 조회 (삭제된 회원도 포함되어야 함)
413+
ResultActions result = mvc
414+
.perform(get("/members"))
415+
.andDo(print());
416+
417+
result
418+
.andExpect(status().isOk())
419+
.andExpect(jsonPath("$.resultCode").value("200-18"))
420+
.andExpect(jsonPath("$.data.members").isArray())
421+
.andExpect(jsonPath("$.data.members[?(@.email == '[email protected]')].isDeleted").value(true));
422+
}
423+
424+
@Test
425+
@DisplayName("관리자가 아닌 사용자의 회원 목록 조회 시도 - 실패")
426+
@WithMockUser(roles = "MENTEE")
427+
void t18() throws Exception {
428+
// 일반 사용자가 회원 목록 조회 시도
429+
ResultActions result = mvc
430+
.perform(get("/members"))
431+
.andDo(print());
432+
433+
result.andExpect(status().isForbidden()); // 403 Forbidden 예상
434+
}
435+
436+
@Test
437+
@DisplayName("로그인하지 않은 상태에서 회원 목록 조회 시도 - 실패")
438+
void t19() throws Exception {
439+
// 인증 없이 회원 목록 조회 시도
440+
ResultActions result = mvc
441+
.perform(get("/members"))
442+
.andDo(print());
443+
444+
result.andExpect(status().is4xxClientError());
445+
}
446+
447+
@Test
448+
@DisplayName("관리자 회원 목록 조회 - 생성일 역순 정렬 확인")
449+
@WithMockUser(roles = "ADMIN")
450+
void t20() throws Exception {
451+
// 시간 차이를 두고 회원 생성
452+
Member member1 = memberService.joinMentee("[email protected]", "첫번째유저", "첫번째닉네임", "password123", "Backend");
453+
Thread.sleep(100); // 시간 차이를 위한 대기
454+
Member member2 = memberService.joinMentee("[email protected]", "두번째유저", "두번째닉네임", "password123", "Frontend");
455+
456+
// 회원 목록 조회 (최신순 정렬)
457+
ResultActions result = mvc
458+
.perform(get("/members")
459+
.param("size", "10"))
460+
.andDo(print());
461+
462+
result
463+
.andExpect(status().isOk())
464+
.andExpect(jsonPath("$.data.members").isArray());
465+
// 최신 회원이 먼저 나와야 함 (createDate DESC)
466+
}
298467
}

0 commit comments

Comments
 (0)