diff --git a/src/main/java/clap/server/adapter/inbound/web/admin/FindMemberController.java b/src/main/java/clap/server/adapter/inbound/web/admin/FindMemberController.java new file mode 100644 index 00000000..28a18ec7 --- /dev/null +++ b/src/main/java/clap/server/adapter/inbound/web/admin/FindMemberController.java @@ -0,0 +1,58 @@ +package clap.server.adapter.inbound.web.admin; + +import clap.server.adapter.inbound.web.dto.admin.FindMemberRequest; +import clap.server.adapter.inbound.web.dto.admin.RetrieveAllMemberResponse; +import clap.server.application.mapper.RetrieveAllMemberMapper; +import clap.server.application.port.inbound.management.FindAllMembersUsecase; +import clap.server.application.port.inbound.management.FindMembersWithFilterUsecase; +import clap.server.domain.model.member.Member; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.http.ResponseEntity; +import org.springframework.security.access.annotation.Secured; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequestMapping("/api/managements") +@RequiredArgsConstructor +public class FindMemberController { + private final FindAllMembersUsecase findAllMembersUsecase; + private final FindMembersWithFilterUsecase findMembersWithFilterUsecase; + private final RetrieveAllMemberMapper retrieveAllMemberMapper; + + @Tag(name = "05. Admin") + @Secured({"ROLE_ADMIN"}) + @Operation( + summary = "전체 회원 조회 API", + description = "모든 회원 정보를 페이징 처리하여 반환하거나 조건에 맞는 회원 정보를 반환합니다.", + parameters = { + @Parameter(name = "page", description = "조회할 페이지 번호 (0부터 시작, 기본값: 0)", example = "0"), + @Parameter(name = "size", description = "페이지 당 회원 수 (기본값: 20)", example = "20") + } + ) + @GetMapping("/members") + public ResponseEntity> getAllMembers( + @RequestParam(defaultValue = "0") int page, + @RequestParam(defaultValue = "20") int size, + @ModelAttribute FindMemberRequest filterRequest) { + + Pageable pageable = PageRequest.of(page, size); + Page members; + + if (filterRequest.getName() != null || filterRequest.getEmail() != null || filterRequest.getNickname() != null || + filterRequest.getDepartmentId() != null || filterRequest.getRole() != null) { + members = findMembersWithFilterUsecase.findMembersWithFilter(pageable, filterRequest); + } else { + members = findAllMembersUsecase.findAllMembers(pageable); + } + + + Page response = members.map(retrieveAllMemberMapper::toResponse); + return ResponseEntity.ok(response); + } +} \ No newline at end of file diff --git a/src/main/java/clap/server/adapter/inbound/web/dto/admin/FindMemberRequest.java b/src/main/java/clap/server/adapter/inbound/web/dto/admin/FindMemberRequest.java new file mode 100644 index 00000000..f94708ac --- /dev/null +++ b/src/main/java/clap/server/adapter/inbound/web/dto/admin/FindMemberRequest.java @@ -0,0 +1,23 @@ +package clap.server.adapter.inbound.web.dto.admin; + +import clap.server.adapter.outbound.persistense.entity.member.constant.MemberRole; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +@Data +public class FindMemberRequest { + @Schema(description = "회원 이름", example = "양시훈") + private String name; + + @Schema(description = "회원 이메일", example = "sihun123@gmail.com") + private String email; + + @Schema(description = "회원 닉네임", example = "leo.sh") + private String nickname; + + @Schema(description = "부서 ID", example = "1") + private Long departmentId; + + @Schema(description = "회원 역할", example = "ROLE_USER") + private MemberRole role; +} diff --git a/src/main/java/clap/server/adapter/inbound/web/dto/admin/RetrieveAllMemberResponse.java b/src/main/java/clap/server/adapter/inbound/web/dto/admin/RetrieveAllMemberResponse.java new file mode 100644 index 00000000..33c1e08f --- /dev/null +++ b/src/main/java/clap/server/adapter/inbound/web/dto/admin/RetrieveAllMemberResponse.java @@ -0,0 +1,27 @@ +package clap.server.adapter.inbound.web.dto.admin; + +import clap.server.adapter.outbound.persistense.entity.member.constant.MemberRole; +import io.swagger.v3.oas.annotations.media.Schema; + +public record RetrieveAllMemberResponse( + @Schema(description = "회원 이름", example = "양시훈") + String name, + + @Schema(description = "회원 이메일", example = "sihun123@gmail.com") + String email, + + @Schema(description = "회원 닉네임, 로그인할 때 쓰입니다.", example = "leo.sh") + String nickname, + + @Schema(description = "승인 권한 여부", example = "true") + Boolean isReviewer, + + @Schema(description = "부서 ID", example = "1") + Long departmentId, + + @Schema(description = "회원 역할", example = "ROLE_USER") + MemberRole role, + + @Schema(description = "회원 직책", example = "개발자") + String departmentRole +) {} diff --git a/src/main/java/clap/server/adapter/outbound/persistense/MemberPersistenceAdapter.java b/src/main/java/clap/server/adapter/outbound/persistense/MemberPersistenceAdapter.java index f07be47c..a71a91bf 100644 --- a/src/main/java/clap/server/adapter/outbound/persistense/MemberPersistenceAdapter.java +++ b/src/main/java/clap/server/adapter/outbound/persistense/MemberPersistenceAdapter.java @@ -1,5 +1,6 @@ package clap.server.adapter.outbound.persistense; +import clap.server.adapter.inbound.web.dto.admin.FindMemberRequest; import clap.server.adapter.outbound.persistense.entity.member.MemberEntity; import clap.server.adapter.outbound.persistense.entity.member.constant.MemberStatus; import clap.server.adapter.outbound.persistense.mapper.MemberPersistenceMapper; @@ -7,18 +8,37 @@ import clap.server.application.port.outbound.member.CommandMemberPort; import clap.server.application.port.outbound.member.LoadMemberPort; import clap.server.common.annotation.architecture.PersistenceAdapter; +import clap.server.domain.model.task.Task; +import clap.server.adapter.outbound.persistense.entity.task.constant.TaskStatus ; +import clap.server.adapter.outbound.persistense.entity.task.TaskEntity; +import clap.server.adapter.outbound.persistense.repository.task.TaskRepository; +import clap.server.adapter.outbound.persistense.mapper.TaskPersistenceMapper; + +import java.util.stream.Collectors; +import java.util.List; + import clap.server.domain.model.member.Member; +import com.querydsl.core.BooleanBuilder; +import com.querydsl.jpa.impl.JPAQueryFactory; import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.Pageable; import java.util.List; import java.util.Optional; import java.util.stream.Collectors; +import static clap.server.adapter.outbound.persistense.entity.member.QMemberEntity.memberEntity; + @PersistenceAdapter @RequiredArgsConstructor public class MemberPersistenceAdapter implements LoadMemberPort, CommandMemberPort { private final MemberRepository memberRepository; private final MemberPersistenceMapper memberPersistenceMapper; + private final TaskRepository taskRepository; + private final TaskPersistenceMapper taskPersistenceMapper; + private final JPAQueryFactory jpaQueryFactory; @Override @@ -60,5 +80,64 @@ public void save(final Member member) { memberRepository.save(memberEntity); } + @Override + public Page findAllMembers(Pageable pageable) { + return executeQueryWithPageable(pageable, new BooleanBuilder().and(memberEntity.status.ne(MemberStatus.DELETED))); + } + + @Override + public Page findMembersWithFilter(Pageable pageable, FindMemberRequest filterRequest) { + BooleanBuilder whereClause = createMemberFilter(filterRequest); + return executeQueryWithPageable(pageable, whereClause); + } + + // 공통 쿼리 처리 + private Page executeQueryWithPageable(Pageable pageable, BooleanBuilder whereClause) { + List entities = jpaQueryFactory + .selectFrom(memberEntity) + .where(whereClause) + .offset(pageable.getOffset()) + .limit(pageable.getPageSize()) + .fetch(); + + long total = jpaQueryFactory + .select(memberEntity.count()) + .from(memberEntity) + .where(whereClause) + .fetchOne(); + + return new PageImpl<>( + entities.stream() + .map(memberPersistenceMapper::toDomain) + .toList(), + pageable, + total + ); + } + + // 필터 조건 생성 + private BooleanBuilder createMemberFilter(FindMemberRequest filterRequest) { + BooleanBuilder whereClause = new BooleanBuilder(); + whereClause.and(memberEntity.status.ne(MemberStatus.DELETED)); + + if (filterRequest.getName() != null) { + whereClause.and(memberEntity.name.containsIgnoreCase(filterRequest.getName())); + } + if (filterRequest.getEmail() != null) { + whereClause.and(memberEntity.email.containsIgnoreCase(filterRequest.getEmail())); + } + if (filterRequest.getNickname() != null) { + whereClause.and(memberEntity.nickname.containsIgnoreCase(filterRequest.getNickname())); + } + if (filterRequest.getDepartmentId() != null) { + whereClause.and(memberEntity.department.departmentId.eq(filterRequest.getDepartmentId())); + } + if (filterRequest.getRole() != null) { + whereClause.and(memberEntity.role.eq(filterRequest.getRole())); + } + + return whereClause; + } } + diff --git a/src/main/java/clap/server/adapter/outbound/persistense/repository/member/MemberRepository.java b/src/main/java/clap/server/adapter/outbound/persistense/repository/member/MemberRepository.java index e1bd0b45..3e186438 100644 --- a/src/main/java/clap/server/adapter/outbound/persistense/repository/member/MemberRepository.java +++ b/src/main/java/clap/server/adapter/outbound/persistense/repository/member/MemberRepository.java @@ -4,13 +4,14 @@ import clap.server.adapter.outbound.persistense.entity.member.constant.MemberRole; import clap.server.adapter.outbound.persistense.entity.member.constant.MemberStatus; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.JpaSpecificationExecutor; import org.springframework.stereotype.Repository; import java.util.List; import java.util.Optional; @Repository -public interface MemberRepository extends JpaRepository { +public interface MemberRepository extends JpaRepository, JpaSpecificationExecutor { List findByRoleAndStatus(MemberRole role, MemberStatus status); diff --git a/src/main/java/clap/server/application/mapper/RetrieveAllMemberMapper.java b/src/main/java/clap/server/application/mapper/RetrieveAllMemberMapper.java new file mode 100644 index 00000000..f48de77b --- /dev/null +++ b/src/main/java/clap/server/application/mapper/RetrieveAllMemberMapper.java @@ -0,0 +1,28 @@ +package clap.server.application.mapper; + +import clap.server.adapter.inbound.web.dto.admin.RetrieveAllMemberResponse; +import clap.server.domain.model.member.Member; +import org.springframework.stereotype.Component; + +import java.util.List; + +@Component +public class RetrieveAllMemberMapper { + public List toResponseList(List members) { + return members.stream() + .map(this::toResponse) + .toList(); + } + + public RetrieveAllMemberResponse toResponse(Member member) { + return new RetrieveAllMemberResponse( + member.getMemberInfo().getName(), + member.getMemberInfo().getEmail(), + member.getMemberInfo().getNickname(), + member.getMemberInfo().isReviewer(), + member.getMemberInfo().getDepartment().getDepartmentId(), + member.getMemberInfo().getRole(), + member.getMemberInfo().getDepartmentRole() + ); + } +} \ No newline at end of file diff --git a/src/main/java/clap/server/application/port/inbound/domain/MemberService.java b/src/main/java/clap/server/application/port/inbound/domain/MemberService.java index 246b122d..001bf53e 100644 --- a/src/main/java/clap/server/application/port/inbound/domain/MemberService.java +++ b/src/main/java/clap/server/application/port/inbound/domain/MemberService.java @@ -3,6 +3,7 @@ import clap.server.application.port.outbound.member.CommandMemberPort; import clap.server.application.port.outbound.member.LoadMemberPort; import clap.server.domain.model.member.Member; +import clap.server.adapter.outbound.persistense.entity.task.constant.TaskStatus; import clap.server.exception.ApplicationException; import clap.server.exception.code.MemberErrorCode; import lombok.RequiredArgsConstructor; diff --git a/src/main/java/clap/server/application/port/inbound/management/FindAllMembersUsecase.java b/src/main/java/clap/server/application/port/inbound/management/FindAllMembersUsecase.java new file mode 100644 index 00000000..c60dc381 --- /dev/null +++ b/src/main/java/clap/server/application/port/inbound/management/FindAllMembersUsecase.java @@ -0,0 +1,10 @@ +package clap.server.application.port.inbound.management; + +import clap.server.domain.model.member.Member; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; + + +public interface FindAllMembersUsecase { + Page findAllMembers(Pageable pageable); +} diff --git a/src/main/java/clap/server/application/port/inbound/management/FindMembersWithFilterUsecase.java b/src/main/java/clap/server/application/port/inbound/management/FindMembersWithFilterUsecase.java new file mode 100644 index 00000000..5ebcd2f7 --- /dev/null +++ b/src/main/java/clap/server/application/port/inbound/management/FindMembersWithFilterUsecase.java @@ -0,0 +1,10 @@ +package clap.server.application.port.inbound.management; + +import clap.server.adapter.inbound.web.dto.admin.FindMemberRequest; +import clap.server.domain.model.member.Member; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; + +public interface FindMembersWithFilterUsecase { + Page findMembersWithFilter(Pageable pageable, FindMemberRequest filterRequest); +} diff --git a/src/main/java/clap/server/application/port/outbound/member/LoadMemberPort.java b/src/main/java/clap/server/application/port/outbound/member/LoadMemberPort.java index 70c04c75..164aa81a 100644 --- a/src/main/java/clap/server/application/port/outbound/member/LoadMemberPort.java +++ b/src/main/java/clap/server/application/port/outbound/member/LoadMemberPort.java @@ -1,6 +1,11 @@ package clap.server.application.port.outbound.member; +import clap.server.adapter.inbound.web.dto.admin.FindMemberRequest; import clap.server.domain.model.member.Member; +import clap.server.domain.model.task.Task; +import clap.server.adapter.outbound.persistense.entity.task.constant.TaskStatus; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; import java.util.List; import java.util.Optional; @@ -14,6 +19,10 @@ public interface LoadMemberPort { List findReviewers(); + Page findAllMembers(Pageable pageable); + + Page findMembersWithFilter(Pageable pageable, FindMemberRequest filterRequest); + Optional findReviewerById(Long id); } diff --git a/src/main/java/clap/server/application/service/member/FindAllMembersService.java b/src/main/java/clap/server/application/service/member/FindAllMembersService.java new file mode 100644 index 00000000..4467bd84 --- /dev/null +++ b/src/main/java/clap/server/application/service/member/FindAllMembersService.java @@ -0,0 +1,20 @@ +package clap.server.application.service.member; + +import clap.server.application.port.inbound.management.FindAllMembersUsecase; +import clap.server.application.port.outbound.member.LoadMemberPort; +import clap.server.domain.model.member.Member; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class FindAllMembersService implements FindAllMembersUsecase { + private final LoadMemberPort loadMemberPort; + + @Override + public Page findAllMembers(Pageable pageable) { + return loadMemberPort.findAllMembers(pageable); + } +} diff --git a/src/main/java/clap/server/application/service/member/FindMembersWithFilterService.java b/src/main/java/clap/server/application/service/member/FindMembersWithFilterService.java new file mode 100644 index 00000000..c13176e7 --- /dev/null +++ b/src/main/java/clap/server/application/service/member/FindMembersWithFilterService.java @@ -0,0 +1,21 @@ +package clap.server.application.service.member; + +import clap.server.adapter.inbound.web.dto.admin.FindMemberRequest; +import clap.server.application.port.inbound.management.FindMembersWithFilterUsecase; +import clap.server.application.port.outbound.member.LoadMemberPort; +import clap.server.domain.model.member.Member; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class FindMembersWithFilterService implements FindMembersWithFilterUsecase { + private final LoadMemberPort loadMemberPort; + + @Override + public Page findMembersWithFilter(Pageable pageable, FindMemberRequest filterRequest) { + return loadMemberPort.findMembersWithFilter(pageable, filterRequest); + } +}