Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.back.domain.member.member.controller;

import com.back.domain.member.member.dto.MemberPagingResponse;
import com.back.domain.member.member.dto.MemberSearchResponse;
import com.back.domain.member.member.dto.MentorUpdateRequest;
import com.back.domain.member.member.dto.MenteeUpdateRequest;
Expand All @@ -21,6 +22,16 @@ public class AdmMemberController {
private final Rq rq;


@GetMapping
@Operation(summary = "회원 목록 조회 (관리자) - 페이징, 페이지는 기본으로 10개씩 조회")
@PreAuthorize("hasRole('ADMIN')")
public RsData<MemberPagingResponse> getAllMembers(
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "10") int size) {
MemberPagingResponse members = memberService.getAllMembersForAdmin(page, size);
return new RsData<>("200-18", "회원 목록 조회 성공", members);
}

@GetMapping("/{memberId}")
@Operation(summary = "회원 상세 조회 (관리자)")
@PreAuthorize("hasRole('ADMIN')")
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package com.back.domain.member.member.dto;

import com.back.domain.member.member.entity.Member;

import java.time.LocalDateTime;

public record MemberListResponse(
Long id,
String email,
String name,
String nickname,
Member.Role role,
Boolean isDeleted,
LocalDateTime createdAt
) {
public static MemberListResponse from(Member member) {
return new MemberListResponse(
member.getId(),
member.getEmail(),
member.getName(),
member.getNickname(),
member.getRole(),
member.getIsDeleted(),
member.getCreateDate()
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package com.back.domain.member.member.dto;

import io.swagger.v3.oas.annotations.media.Schema;
import org.springframework.data.domain.Page;

import java.util.List;

public record MemberPagingResponse(
@Schema(description = "회원 목록")
List<MemberListResponse> members,
@Schema(description = "현재 페이지 (0부터 시작)")
int currentPage,
@Schema(description = "총 페이지")
int totalPage,
@Schema(description = "총 개수")
long totalElements,
@Schema(description = "다음 페이지 존재 여부")
boolean hasNext
) {

public static MemberPagingResponse from(Page<MemberListResponse> page) {
return new MemberPagingResponse(
page.getContent(),
page.getNumber(),
page.getTotalPages(),
page.getTotalElements(),
page.hasNext()
);
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package com.back.domain.member.member.repository;

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

@Query("SELECT m FROM Member m WHERE m.id = :id")
Optional<Member> findByIdIncludingDeleted(@Param("id") Long id);

@Query("SELECT m FROM Member m ORDER BY m.createDate DESC")
Page<Member> findAllIncludingDeleted(Pageable pageable);
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@
import com.back.domain.member.mentor.repository.MentorRepository;
import com.back.global.exception.ServiceException;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
Expand Down Expand Up @@ -276,6 +279,14 @@ public void updateMentor(Member currentUser, MentorUpdateRequest request) {
}


public MemberPagingResponse getAllMembersForAdmin(int page, int size) {
Pageable pageable = PageRequest.of(page, size);
Page<Member> members = memberRepository.findAllIncludingDeleted(pageable);

Page<MemberListResponse> memberPage = members.map(MemberListResponse::from);
return MemberPagingResponse.from(memberPage);
}

public MemberSearchResponse getMemberForAdmin(Long memberId) {
Member member = memberRepository.findByIdIncludingDeleted(memberId)
.orElseThrow(() -> new ServiceException("404-1", "존재하지 않는 회원입니다."));
Expand Down
41 changes: 41 additions & 0 deletions back/src/main/java/com/back/global/initData/AdminInitData.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package com.back.global.initData;

import com.back.domain.member.member.entity.Member;
import com.back.domain.member.member.repository.MemberRepository;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.ApplicationRunner;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Profile;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Component;

@Component
@Profile("dev")
@RequiredArgsConstructor
@Slf4j
public class AdminInitData {

private final MemberRepository memberRepository;
private final PasswordEncoder passwordEncoder;

@Bean
public ApplicationRunner initAdmin() {
return args -> {
if (memberRepository.findByEmail("[email protected]").isEmpty()) {
Member admin = new Member(
"[email protected]",
passwordEncoder.encode("admin1234"),
"관리자",
"admin",
Member.Role.ADMIN
);
memberRepository.save(admin);
log.info("어드민 계정이 생성되었습니다. (email: [email protected], password: admin1234)");
} else {
log.info("어드민 계정이 이미 존재합니다.");
}
};
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -295,4 +295,173 @@ void t12() throws Exception {

result.andExpect(status().is4xxClientError());
}

@Test
@DisplayName("관리자 회원 목록 조회 성공 - 기본 페이징")
@WithMockUser(roles = "ADMIN")
void t13() throws Exception {
// 테스트용 회원 3명 생성
memberService.joinMentee("[email protected]", "멘티1", "멘티닉네임1", "password123", "Backend");
memberService.joinMentor("[email protected]", "멘토1", "멘토닉네임1", "password123", "Frontend", 3);
memberService.joinMentee("[email protected]", "멘티2", "멘티닉네임2", "password123", "AI/ML");

// 관리자가 회원 목록 조회 (기본값: page=0, size=10)
ResultActions result = mvc
.perform(get("/members"))
.andDo(print());

result
.andExpect(handler().handlerType(AdmMemberController.class))
.andExpect(handler().methodName("getAllMembers"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.resultCode").value("200-18"))
.andExpect(jsonPath("$.msg").value("회원 목록 조회 성공"))
.andExpect(jsonPath("$.data.members").isArray())
.andExpect(jsonPath("$.data.totalElements").exists())
.andExpect(jsonPath("$.data.totalPage").exists())
.andExpect(jsonPath("$.data.currentPage").value(0))
.andExpect(jsonPath("$.data.hasNext").exists());
}

@Test
@DisplayName("관리자 회원 목록 조회 성공 - 커스텀 페이징")
@WithMockUser(roles = "ADMIN")
void t14() throws Exception {
// 테스트용 회원 5명 생성
memberService.joinMentee("[email protected]", "유저1", "닉네임1", "password123", "Backend");
memberService.joinMentee("[email protected]", "유저2", "닉네임2", "password123", "Frontend");
memberService.joinMentor("[email protected]", "유저3", "닉네임3", "password123", "Backend", 2);
memberService.joinMentor("[email protected]", "유저4", "닉네임4", "password123", "AI/ML", 5);
memberService.joinMentee("[email protected]", "유저5", "닉네임5", "password123", "DevOps");

// 페이지 크기 2로 첫 페이지 조회
ResultActions result = mvc
.perform(get("/members")
.param("page", "0")
.param("size", "2"))
.andDo(print());

result
.andExpect(status().isOk())
.andExpect(jsonPath("$.resultCode").value("200-18"))
.andExpect(jsonPath("$.data.members").isArray())
.andExpect(jsonPath("$.data.currentPage").value(0))
.andExpect(jsonPath("$.data.members.length()").value(2));
}

@Test
@DisplayName("관리자 회원 목록 조회 - 두 번째 페이지")
@WithMockUser(roles = "ADMIN")
void t15() throws Exception {
// 테스트용 회원 5명 생성
memberService.joinMentee("[email protected]", "페이지유저1", "페이지닉네임1", "password123", "Backend");
memberService.joinMentee("[email protected]", "페이지유저2", "페이지닉네임2", "password123", "Frontend");
memberService.joinMentor("[email protected]", "페이지유저3", "페이지닉네임3", "password123", "Backend", 3);
memberService.joinMentor("[email protected]", "페이지유저4", "페이지닉네임4", "password123", "AI/ML", 4);
memberService.joinMentee("[email protected]", "페이지유저5", "페이지닉네임5", "password123", "DevOps");

// 페이지 크기 2로 두 번째 페이지 조회
ResultActions result = mvc
.perform(get("/members")
.param("page", "1")
.param("size", "2"))
.andDo(print());

result
.andExpect(status().isOk())
.andExpect(jsonPath("$.resultCode").value("200-18"))
.andExpect(jsonPath("$.data.members").isArray())
.andExpect(jsonPath("$.data.currentPage").value(1));
}

@Test
@DisplayName("관리자 회원 목록 조회 - 기본 정보만 포함 확인")
@WithMockUser(roles = "ADMIN")
void t16() throws Exception {
// 멘토와 멘티 생성
memberService.joinMentor("[email protected]", "멘토유저", "멘토닉네임", "password123", "Backend", 5);
memberService.joinMentee("[email protected]", "멘티유저", "멘티닉네임", "password123", "Frontend");

// 회원 목록 조회
ResultActions result = mvc
.perform(get("/members")
.param("size", "10"))
.andDo(print());

result
.andExpect(status().isOk())
.andExpect(jsonPath("$.data.members").isArray())
// 기본 정보만 포함되어야 함
.andExpect(jsonPath("$.data.members[0].id").exists())
.andExpect(jsonPath("$.data.members[0].email").exists())
.andExpect(jsonPath("$.data.members[0].name").exists())
.andExpect(jsonPath("$.data.members[0].nickname").exists())
.andExpect(jsonPath("$.data.members[0].role").exists())
.andExpect(jsonPath("$.data.members[0].isDeleted").exists())
.andExpect(jsonPath("$.data.members[0].createdAt").exists());
}

@Test
@DisplayName("관리자 회원 목록 조회 - 삭제된 회원도 포함")
@WithMockUser(roles = "ADMIN")
void t17() throws Exception {
// 회원 생성 후 삭제
Member member = memberService.joinMentee("[email protected]", "삭제될유저", "삭제될닉네임", "password123", "Backend");
memberService.deleteMemberByAdmin(member.getId());

// 회원 목록 조회 (삭제된 회원도 포함되어야 함)
ResultActions result = mvc
.perform(get("/members"))
.andDo(print());

result
.andExpect(status().isOk())
.andExpect(jsonPath("$.resultCode").value("200-18"))
.andExpect(jsonPath("$.data.members").isArray())
.andExpect(jsonPath("$.data.members[?(@.email == '[email protected]')].isDeleted").value(true));
}

@Test
@DisplayName("관리자가 아닌 사용자의 회원 목록 조회 시도 - 실패")
@WithMockUser(roles = "MENTEE")
void t18() throws Exception {
// 일반 사용자가 회원 목록 조회 시도
ResultActions result = mvc
.perform(get("/members"))
.andDo(print());

result.andExpect(status().isForbidden()); // 403 Forbidden 예상
}

@Test
@DisplayName("로그인하지 않은 상태에서 회원 목록 조회 시도 - 실패")
void t19() throws Exception {
// 인증 없이 회원 목록 조회 시도
ResultActions result = mvc
.perform(get("/members"))
.andDo(print());

result.andExpect(status().is4xxClientError());
}

@Test
@DisplayName("관리자 회원 목록 조회 - 생성일 역순 정렬 확인")
@WithMockUser(roles = "ADMIN")
void t20() throws Exception {
// 시간 차이를 두고 회원 생성
Member member1 = memberService.joinMentee("[email protected]", "첫번째유저", "첫번째닉네임", "password123", "Backend");
Thread.sleep(100); // 시간 차이를 위한 대기
Member member2 = memberService.joinMentee("[email protected]", "두번째유저", "두번째닉네임", "password123", "Frontend");

// 회원 목록 조회 (최신순 정렬)
ResultActions result = mvc
.perform(get("/members")
.param("size", "10"))
.andDo(print());

result
.andExpect(status().isOk())
.andExpect(jsonPath("$.data.members").isArray());
// 최신 회원이 먼저 나와야 함 (createDate DESC)
}
}