diff --git a/src/main/java/greenfirst/be/user/adapter/in/web/controller/GetUserDataController.java b/src/main/java/greenfirst/be/user/adapter/in/web/controller/GetUserDataController.java index 4066138..3a35e14 100644 --- a/src/main/java/greenfirst/be/user/adapter/in/web/controller/GetUserDataController.java +++ b/src/main/java/greenfirst/be/user/adapter/in/web/controller/GetUserDataController.java @@ -6,12 +6,17 @@ import greenfirst.be.global.common.security.CustomUserDetails; import greenfirst.be.user.adapter.in.web.response.UserProfileResponse; import greenfirst.be.user.adapter.in.web.response.UserSimpleDataListResponse; +import greenfirst.be.user.adapter.in.web.response.WithdrawUserListResponse; import greenfirst.be.user.application.dto.in.GetUserByUuidInDto; import greenfirst.be.user.application.dto.in.GetUserSimpleDataListInDto; +import greenfirst.be.user.application.dto.in.GetWithdrawUserListInDto; import greenfirst.be.user.application.dto.out.UserDataListOutDto; +import greenfirst.be.user.application.dto.out.WithdrawUserListOutDto; import greenfirst.be.user.application.service.GetUserDataService; import greenfirst.be.user.domain.model.Users; import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.security.SecurityRequirement; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -44,6 +49,7 @@ public class GetUserDataController { * 1. (admin/agency) 전체 유저 목록 조회 * 2. (common) 본인 프로필 조회 * 3. (admin/agency) 다른 유저 프로필 조회 + * 4. (admin) 탈퇴 유저 목록 조회 */ // 1. (admin/agency) 유저 목록 조회/검색 @@ -79,6 +85,56 @@ public BaseResponse getUserList( } + // 4. (admin) 탈퇴 유저 목록 조회 + @Operation(summary = "(관리자) 탈퇴 유저 목록 조회/검색", description = "탈퇴 유저 목록을 타입별로 조회하거나, 이름/휴대폰 번호로 검색 진행", tags = { "User - Admin" }) + @GetMapping("/withdraw/list") + @PreAuthorize("hasAuthority('ADMIN')") + @SecurityRequirement(name = "Bearer Auth") + public BaseResponse getWithdrawUserList( + @Parameter(description = "이름 부분 검색 (null/blank면 미적용)") @RequestParam(required = false) String userName, + @Parameter(description = "전화번호 부분 검색 (null/blank면 미적용)") @RequestParam(required = false) String userPhoneNumber, + @Parameter(description = "탈퇴 회원 타입 필터 (미입력 시 USER/PERSONAL_PARTNER/CORPORATE_PARTNER/AGENCY 기본 적용)") + @Schema(allowableValues = { "USER", "PERSONAL_PARTNER", "CORPORATE_PARTNER", "AGENCY" }) + @RequestParam(required = false) List userTypeList, + @Parameter(description = "탈퇴 시기 정렬 (기본 DESC)") + @Schema(allowableValues = { "ASC", "DESC" }) + @RequestParam(required = false) String orderType, + @Nullable @PageableDefault Pageable pageable, + @AuthenticationPrincipal CustomUserDetails authentication + ) { + + // default user types + List allowedUserTypes = List.of(UserType.USER, UserType.PERSONAL_PARTNER, UserType.CORPORATE_PARTNER, UserType.AGENCY); + List resolvedUserTypes = (userTypeList == null || userTypeList.isEmpty()) + ? allowedUserTypes + : userTypeList.stream() + .filter(allowedUserTypes::contains) + .distinct() + .toList(); + + if (resolvedUserTypes.isEmpty()) { + resolvedUserTypes = allowedUserTypes; + } + + // mapping + GetWithdrawUserListInDto inDto = GetWithdrawUserListInDto.builder() + .userName(userName) + .userPhoneNumber(userPhoneNumber) + .userTypeList(resolvedUserTypes) + .orderType(orderType) + .pageable(pageable) + .requestorUuid(authentication.getUserUuid()) + .build(); + + // 탈퇴 유저 목록 조회 + WithdrawUserListOutDto outDto = getUserDataService.getWithdrawUserList(inDto); + + // result + WithdrawUserListResponse response = WithdrawUserListResponse.from(outDto); + return new BaseResponse<>(response); + } + + // 2. (common) 본인 프로필 조회 @Operation(summary = "본인 프로필 조회", description = "로그인한 사용자 본인의 프로필 정보를 조회합니다", tags = { "User - Admin", "User - Agency", "User - Partner", "User - User" }) @GetMapping("/profile/my") diff --git a/src/main/java/greenfirst/be/user/adapter/in/web/response/WithdrawUserListResponse.java b/src/main/java/greenfirst/be/user/adapter/in/web/response/WithdrawUserListResponse.java new file mode 100644 index 0000000..540bd60 --- /dev/null +++ b/src/main/java/greenfirst/be/user/adapter/in/web/response/WithdrawUserListResponse.java @@ -0,0 +1,37 @@ +package greenfirst.be.user.adapter.in.web.response; + + +import greenfirst.be.global.common.response.pagination.CustomPaginationResponse; +import greenfirst.be.user.application.dto.out.WithdrawUserListOutDto; +import lombok.AllArgsConstructor; +import lombok.NoArgsConstructor; +import lombok.Getter; +import lombok.experimental.SuperBuilder; + +import java.util.List; + + +@Getter +@SuperBuilder(toBuilder = true) +@NoArgsConstructor +@AllArgsConstructor +public class WithdrawUserListResponse extends CustomPaginationResponse { + + private List content; + + public static WithdrawUserListResponse from(WithdrawUserListOutDto outDto) { + + List content = outDto.getContent().stream() + .map(WithdrawUserResponse::from) + .toList(); + + return WithdrawUserListResponse.builder() + .content(content) + .totalElementCount(outDto.getTotalElementCount()) + .totalPageCount(outDto.getTotalPageCount()) + .number(outDto.getNumber()) + .size(outDto.getSize()) + .build(); + } + +} diff --git a/src/main/java/greenfirst/be/user/adapter/in/web/response/WithdrawUserResponse.java b/src/main/java/greenfirst/be/user/adapter/in/web/response/WithdrawUserResponse.java new file mode 100644 index 0000000..56b693d --- /dev/null +++ b/src/main/java/greenfirst/be/user/adapter/in/web/response/WithdrawUserResponse.java @@ -0,0 +1,38 @@ +package greenfirst.be.user.adapter.in.web.response; + + +import greenfirst.be.user.application.dto.out.WithdrawUserSimpleDataOutDto; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; +import java.util.UUID; + + +@Getter +@Builder(toBuilder = true) +@NoArgsConstructor +@AllArgsConstructor +public class WithdrawUserResponse { + + private UUID userUuid; + private String userType; + private String userName; + private String userPhoneNumber; + private String userLoginId; + private LocalDateTime deletedAt; + + public static WithdrawUserResponse from(WithdrawUserSimpleDataOutDto outDto) { + return WithdrawUserResponse.builder() + .userUuid(outDto.getUserUuid()) + .userType(outDto.getUserType()) + .userName(outDto.getUserName()) + .userPhoneNumber(outDto.getUserPhoneNumber()) + .userLoginId(outDto.getUserLoginId()) + .deletedAt(outDto.getDeletedAt()) + .build(); + } + +} diff --git a/src/main/java/greenfirst/be/user/adapter/out/persistence/entity/WithDrawlUser.java b/src/main/java/greenfirst/be/user/adapter/out/persistence/entity/WithDrawlUser.java deleted file mode 100644 index 541484c..0000000 --- a/src/main/java/greenfirst/be/user/adapter/out/persistence/entity/WithDrawlUser.java +++ /dev/null @@ -1,54 +0,0 @@ -package greenfirst.be.user.adapter.out.persistence.entity; - - -import greenfirst.be.global.common.basetime.BaseTimeEntity; -import greenfirst.be.global.common.enums.common.UserType; -import greenfirst.be.user.adapter.out.persistence.entity.vo.PartnerOptionVo; -import greenfirst.be.user.adapter.out.persistence.entity.vo.UserAddressVo; -import jakarta.persistence.*; -import lombok.*; - -import java.time.LocalDateTime; -import java.util.UUID; - - -@Table(name = "withdrawl_user") -@Getter -@Entity -@Builder(toBuilder = true) -@NoArgsConstructor(access = AccessLevel.PROTECTED) -@AllArgsConstructor(access = AccessLevel.PRIVATE) -public class WithDrawlUser extends BaseTimeEntity { - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; - - @Column(name = "user_type", length = 30, nullable = false) - private UserType userType; - - @Column(name = "user_uuid", nullable = false, columnDefinition = "BINARY(16)", unique = true) - private UUID userUuid; - - @Column(name = "user_name", length = 100, nullable = false) - private String userName; - - @Column(name = "user_phone_number", length = 20, nullable = false) - private String userPhoneNumber; - - @Column(name = "user_login_id", length = 100, nullable = false, unique = true) - private String userLoginId; // 개인파트너는 loginId가 휴대폰번호가 됨 - - @Column(name = "user_password", length = 255, nullable = false) - private String userPassword; - - @Column(name = "deleted_at") - private LocalDateTime deletedAt; - - @Embedded - private PartnerOptionVo partnerOption; - - @Embedded - private UserAddressVo userAddress; - -} diff --git a/src/main/java/greenfirst/be/user/adapter/out/persistence/querydsl/UserQuerydslRepository.java b/src/main/java/greenfirst/be/user/adapter/out/persistence/querydsl/UserQuerydslRepository.java index 1f59fcd..adcf9db 100644 --- a/src/main/java/greenfirst/be/user/adapter/out/persistence/querydsl/UserQuerydslRepository.java +++ b/src/main/java/greenfirst/be/user/adapter/out/persistence/querydsl/UserQuerydslRepository.java @@ -10,6 +10,9 @@ import greenfirst.be.user.application.dto.in.GetUserSimpleDataListInDto; import greenfirst.be.user.application.dto.out.UserDataListOutDto; import greenfirst.be.user.application.dto.out.UserSimpleDataOutDto; +import greenfirst.be.user.application.dto.in.GetWithdrawUserListInDto; +import greenfirst.be.user.application.dto.out.WithdrawUserListOutDto; +import greenfirst.be.user.application.dto.out.WithdrawUserSimpleDataOutDto; import greenfirst.be.vehicle.adapter.out.persistence.entity.QVehicleEntity; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -36,6 +39,7 @@ public class UserQuerydslRepository { /** * UserQuerydslRepository * 1. 유저 목록 조회 + * 2. 탈퇴 유저 목록 조회 */ // 1. 유저 목록 조회 @@ -97,4 +101,57 @@ public UserDataListOutDto getUserSimpleDataList(GetUserSimpleDataListInDto inDto } + // 2. 탈퇴 유저 목록 조회 + public WithdrawUserListOutDto getWithdrawUserList(GetWithdrawUserListInDto inDto) { + + // paging + Pageable pageable = inDto.getPageable(); + long offset = pageable.getOffset(); + int pageSize = pageable.getPageSize(); + + // conditions + BooleanBuilder where = new BooleanBuilder() + .and(userUtil.withdrawUserFilter(qUser)) + .and(userUtil.userTypeFilter(qUser, inDto.getUserTypeList())) + .and(userUtil.userNameSearch(qUser, inDto.getUserName())) + .and(userUtil.userPhoneNumberSearch(qUser, inDto.getUserPhoneNumber())); + + // content + List content = queryFactory + .select(Projections.constructor(WithdrawUserSimpleDataOutDto.class, + qUser.userUuid, + qUser.userType, + qUser.userName, + qUser.userPhoneNumber, + qUser.userLoginId, + qUser.deletedAt + )) + .from(qUser) + .where(where) + .offset(offset) + .limit(pageSize) + .orderBy(userUtil.withdrawUserOrder(qUser, inDto.getOrderType())) + .fetch(); + + // total element count + Long totalElementCount = queryFactory + .select(qUser.count()) + .from(qUser) + .where(where) + .fetchFirst(); + + // total page count + long totalPageCount = commonUtil.calculateTotalPage(pageable, totalElementCount); + + // return + return WithdrawUserListOutDto.builder() + .content(content) + .totalElementCount(totalElementCount) + .totalPageCount(totalPageCount) + .number(pageable.getPageNumber() + 1) + .size(pageable.getPageSize()) + .build(); + + } + } diff --git a/src/main/java/greenfirst/be/user/adapter/out/persistence/querydsl/util/UserQuerydslUtil.java b/src/main/java/greenfirst/be/user/adapter/out/persistence/querydsl/util/UserQuerydslUtil.java index 33f38f8..c8cf88e 100644 --- a/src/main/java/greenfirst/be/user/adapter/out/persistence/querydsl/util/UserQuerydslUtil.java +++ b/src/main/java/greenfirst/be/user/adapter/out/persistence/querydsl/util/UserQuerydslUtil.java @@ -2,6 +2,7 @@ import com.querydsl.core.types.dsl.BooleanExpression; +import com.querydsl.core.types.OrderSpecifier; import greenfirst.be.global.common.enums.common.UserType; import greenfirst.be.user.adapter.out.persistence.entity.QUserEntity; import greenfirst.be.vehicle.adapter.out.persistence.entity.QVehicleEntity; @@ -14,6 +15,8 @@ @Component public class UserQuerydslUtil { + private static final String DESC = "DESC"; + // 유저 타입별 필터 public @Nullable BooleanExpression userTypeFilter(QUserEntity qUser, List userTypeList) { @@ -57,4 +60,19 @@ public class UserQuerydslUtil { return qUser.deletedAt.isNull(); } + // 탈퇴 사용자 필터 (삭제된 사용자만) + public @Nullable BooleanExpression withdrawUserFilter(QUserEntity qUser) { + return qUser.deletedAt.isNotNull(); + } + + // 탈퇴일 정렬 + public OrderSpecifier withdrawUserOrder(QUserEntity qUser, String orderType) { + + String normalizedOrderType = orderType == null || orderType.isBlank() || orderType.equalsIgnoreCase(DESC) + ? DESC + : "ASC"; + + return normalizedOrderType.equals(DESC) ? qUser.deletedAt.desc() : qUser.deletedAt.asc(); + } + } diff --git a/src/main/java/greenfirst/be/user/adapter/out/persistence/repository/UserQueryRepositoryImpl.java b/src/main/java/greenfirst/be/user/adapter/out/persistence/repository/UserQueryRepositoryImpl.java index 096210a..5327f0f 100644 --- a/src/main/java/greenfirst/be/user/adapter/out/persistence/repository/UserQueryRepositoryImpl.java +++ b/src/main/java/greenfirst/be/user/adapter/out/persistence/repository/UserQueryRepositoryImpl.java @@ -8,7 +8,9 @@ import greenfirst.be.user.adapter.out.persistence.jpa.UserJpaRepository; import greenfirst.be.user.adapter.out.persistence.querydsl.UserQuerydslRepository; import greenfirst.be.user.application.dto.in.GetUserSimpleDataListInDto; +import greenfirst.be.user.application.dto.in.GetWithdrawUserListInDto; import greenfirst.be.user.application.dto.out.UserDataListOutDto; +import greenfirst.be.user.application.dto.out.WithdrawUserListOutDto; import greenfirst.be.user.application.port.out.repository.UserQueryRepository; import greenfirst.be.user.domain.model.Users; import lombok.RequiredArgsConstructor; @@ -59,6 +61,12 @@ public UserDataListOutDto getUserSimpleDataList(GetUserSimpleDataListInDto inDto return userQuerydslRepository.getUserSimpleDataList(inDto); } + // 조건에 맞는 탈퇴 유저 목록 조회 + @Override + public WithdrawUserListOutDto getWithdrawUserList(GetWithdrawUserListInDto inDto) { + return userQuerydslRepository.getWithdrawUserList(inDto); + } + // 로그인 아이디로 유저 존재 확인 @Override diff --git a/src/main/java/greenfirst/be/user/application/dto/in/GetWithdrawUserListInDto.java b/src/main/java/greenfirst/be/user/application/dto/in/GetWithdrawUserListInDto.java new file mode 100644 index 0000000..ca54482 --- /dev/null +++ b/src/main/java/greenfirst/be/user/application/dto/in/GetWithdrawUserListInDto.java @@ -0,0 +1,24 @@ +package greenfirst.be.user.application.dto.in; + + +import greenfirst.be.global.common.enums.common.UserType; +import lombok.Builder; +import lombok.Getter; +import org.springframework.data.domain.Pageable; + +import java.util.List; +import java.util.UUID; + + +@Getter +@Builder +public class GetWithdrawUserListInDto { + + private String userName; + private String userPhoneNumber; + private List userTypeList; + private String orderType; + private Pageable pageable; + private UUID requestorUuid; + +} diff --git a/src/main/java/greenfirst/be/user/application/dto/out/WithdrawUserListOutDto.java b/src/main/java/greenfirst/be/user/application/dto/out/WithdrawUserListOutDto.java new file mode 100644 index 0000000..da6aef4 --- /dev/null +++ b/src/main/java/greenfirst/be/user/application/dto/out/WithdrawUserListOutDto.java @@ -0,0 +1,17 @@ +package greenfirst.be.user.application.dto.out; + + +import greenfirst.be.global.common.response.pagination.CustomPaginationResponse; +import lombok.Getter; +import lombok.experimental.SuperBuilder; + +import java.util.List; + + +@Getter +@SuperBuilder +public class WithdrawUserListOutDto extends CustomPaginationResponse { + + private List content; + +} diff --git a/src/main/java/greenfirst/be/user/application/dto/out/WithdrawUserSimpleDataOutDto.java b/src/main/java/greenfirst/be/user/application/dto/out/WithdrawUserSimpleDataOutDto.java new file mode 100644 index 0000000..29f2968 --- /dev/null +++ b/src/main/java/greenfirst/be/user/application/dto/out/WithdrawUserSimpleDataOutDto.java @@ -0,0 +1,35 @@ +package greenfirst.be.user.application.dto.out; + + +import com.querydsl.core.annotations.QueryProjection; +import greenfirst.be.global.common.enums.common.UserType; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; +import java.util.UUID; + + +@Getter +@NoArgsConstructor +public class WithdrawUserSimpleDataOutDto { + + private UUID userUuid; + private String userType; + private String userName; + private String userPhoneNumber; + private String userLoginId; + private LocalDateTime deletedAt; + + + @QueryProjection + public WithdrawUserSimpleDataOutDto(UUID userUuid, UserType userType, String userName, String userPhoneNumber, String userLoginId, LocalDateTime deletedAt) { + this.userUuid = userUuid; + this.userType = userType.getDescription(); + this.userName = userName; + this.userPhoneNumber = userPhoneNumber; + this.userLoginId = userLoginId; + this.deletedAt = deletedAt; + } + +} diff --git a/src/main/java/greenfirst/be/user/application/port/out/repository/UserQueryRepository.java b/src/main/java/greenfirst/be/user/application/port/out/repository/UserQueryRepository.java index 362febb..5fed082 100644 --- a/src/main/java/greenfirst/be/user/application/port/out/repository/UserQueryRepository.java +++ b/src/main/java/greenfirst/be/user/application/port/out/repository/UserQueryRepository.java @@ -2,7 +2,9 @@ import greenfirst.be.user.application.dto.in.GetUserSimpleDataListInDto; +import greenfirst.be.user.application.dto.in.GetWithdrawUserListInDto; import greenfirst.be.user.application.dto.out.UserDataListOutDto; +import greenfirst.be.user.application.dto.out.WithdrawUserListOutDto; import greenfirst.be.user.domain.model.Users; import java.util.List; @@ -20,6 +22,9 @@ public interface UserQueryRepository { // 조건에 맞는 유저 목록 조회 UserDataListOutDto getUserSimpleDataList(GetUserSimpleDataListInDto inDto); + // 조건에 맞는 탈퇴 유저 목록 조회 + WithdrawUserListOutDto getWithdrawUserList(GetWithdrawUserListInDto inDto); + // 로그인 아이디로 유저 존재 확인 boolean existsByLoginId(String userLoginId); diff --git a/src/main/java/greenfirst/be/user/application/service/GetUserDataService.java b/src/main/java/greenfirst/be/user/application/service/GetUserDataService.java index ef95d3b..49ca9ff 100644 --- a/src/main/java/greenfirst/be/user/application/service/GetUserDataService.java +++ b/src/main/java/greenfirst/be/user/application/service/GetUserDataService.java @@ -1,10 +1,15 @@ package greenfirst.be.user.application.service; +import greenfirst.be.global.common.enums.common.UserType; +import greenfirst.be.global.common.exception.BaseException; +import greenfirst.be.global.common.response.base.BaseResponseStatus; import greenfirst.be.user.application.dto.in.GetUserByLoginIdInDto; import greenfirst.be.user.application.dto.in.GetUserByUuidInDto; import greenfirst.be.user.application.dto.in.GetUserSimpleDataListInDto; +import greenfirst.be.user.application.dto.in.GetWithdrawUserListInDto; import greenfirst.be.user.application.dto.out.UserDataListOutDto; +import greenfirst.be.user.application.dto.out.WithdrawUserListOutDto; import greenfirst.be.user.application.port.out.repository.UserQueryRepository; import greenfirst.be.user.application.port.out.repository.UserVehicleQueryRepository; import greenfirst.be.user.domain.model.Users; @@ -37,6 +42,7 @@ public class GetUserDataService { * 4. (admin/agency) UUID로 유저 조회 (권한 검증 포함) * 5. 유저 차량 번호 조회 * 6. UUID 리스트로 유저 목록 조회 + * 7. (admin) 탈퇴 유저 목록 조회 */ // 1. 유저 uuid로 유저 조회 @@ -98,4 +104,19 @@ public List getUsersByUuidInList(List uuidList) { return userQueryRepository.getUsersByUuidInList(uuidList); } + // 7. (admin) 탈퇴 유저 목록 조회 + public WithdrawUserListOutDto getWithdrawUserList(GetWithdrawUserListInDto inDto) { + + // 요청자 조회 + Users requestor = userQueryRepository.getByUserUuid(inDto.getRequestorUuid()); + + // 요청자 권한 확인 (admin only) + if (requestor.getUserType() != UserType.ADMIN) { + throw new BaseException(BaseResponseStatus.NO_AUTHORITY_TO_SEARCH_USER); + } + + // 조건에 맞는 탈퇴 유저 목록 조회 + return userQueryRepository.getWithdrawUserList(inDto); + } + } diff --git a/src/test/java/greenfirst/be/user/application/GetUserDataServiceUnitTest.java b/src/test/java/greenfirst/be/user/application/GetUserDataServiceUnitTest.java index 1055a43..53cd66a 100644 --- a/src/test/java/greenfirst/be/user/application/GetUserDataServiceUnitTest.java +++ b/src/test/java/greenfirst/be/user/application/GetUserDataServiceUnitTest.java @@ -2,8 +2,11 @@ import greenfirst.be.global.common.enums.common.UserType; +import greenfirst.be.global.common.exception.BaseException; +import greenfirst.be.user.application.dto.in.GetWithdrawUserListInDto; import greenfirst.be.user.application.dto.in.GetUserSimpleDataListInDto; import greenfirst.be.user.application.dto.out.UserDataListOutDto; +import greenfirst.be.user.application.dto.out.WithdrawUserListOutDto; import greenfirst.be.user.application.service.GetUserDataService; import greenfirst.be.user.domain.model.Users; import greenfirst.be.users.fake.FakeUserQueryRepository; @@ -17,6 +20,7 @@ import org.modelmapper.config.Configuration; import org.springframework.data.domain.PageRequest; +import java.time.LocalDateTime; import java.util.List; import java.util.UUID; @@ -461,4 +465,106 @@ void repositoryCall_GetUserSimpleDataList_Success() { } -} \ No newline at end of file + @Nested + @DisplayName("탈퇴 유저 목록 조회") + class GetWithdrawUserListTest { + + @Test + @DisplayName("성공: 관리자 권한으로 탈퇴 유저 목록을 조회한다") + void getWithdrawUserList_Admin_Success() { + // given + Users admin = UserTestFixture.createAdmin().toBuilder() + .userUuid(UUID.randomUUID()) + .build(); + Users activeUser = UserTestFixture.createUser().toBuilder() + .userUuid(UUID.randomUUID()) + .build(); + Users deletedUser1 = UserTestFixture.createUser().toBuilder() + .userUuid(UUID.randomUUID()) + .userName("탈퇴유저1") + .deletedAt(LocalDateTime.now().minusDays(3)) + .build(); + Users deletedUser2 = UserTestFixture.createPersonalPartner().toBuilder() + .userUuid(UUID.randomUUID()) + .userName("탈퇴유저2") + .deletedAt(LocalDateTime.now().minusDays(1)) + .build(); + + fakeUserQueryRepository.addUser(admin); + fakeUserQueryRepository.addUser(activeUser); + fakeUserQueryRepository.addUser(deletedUser1); + fakeUserQueryRepository.addUser(deletedUser2); + + GetWithdrawUserListInDto inDto = GetWithdrawUserListInDto.builder() + .requestorUuid(admin.getUserUuid()) + .userTypeList(List.of(UserType.USER, UserType.PERSONAL_PARTNER)) + .orderType("DESC") + .pageable(PageRequest.of(0, 10)) + .build(); + + // when + WithdrawUserListOutDto result = getUserDataService.getWithdrawUserList(inDto); + + // then + assertThat(result).isNotNull(); + assertThat(result.getContent()).hasSize(2); + assertThat(result.getContent()).extracting("userName") + .containsExactly("탈퇴유저2", "탈퇴유저1"); + assertThat(result.getTotalElementCount()).isEqualTo(2); + } + + @Test + @DisplayName("실패: 관리자 외 권한은 탈퇴 유저 목록 조회가 불가하다") + void getWithdrawUserList_NonAdmin_ThrowsException() { + // given + Users user = UserTestFixture.createUser().toBuilder() + .userUuid(UUID.randomUUID()) + .build(); + fakeUserQueryRepository.addUser(user); + + GetWithdrawUserListInDto inDto = GetWithdrawUserListInDto.builder() + .requestorUuid(user.getUserUuid()) + .userTypeList(List.of(UserType.USER)) + .pageable(PageRequest.of(0, 10)) + .build(); + + // when & then + assertThrows(BaseException.class, () -> getUserDataService.getWithdrawUserList(inDto)); + } + + @Test + @DisplayName("성공: 탈퇴 유저 타입으로 필터링한다") + void getWithdrawUserList_FilterByUserType_Success() { + // given + Users admin = UserTestFixture.createAdmin().toBuilder() + .userUuid(UUID.randomUUID()) + .build(); + Users deletedUser = UserTestFixture.createUser().toBuilder() + .userUuid(UUID.randomUUID()) + .deletedAt(LocalDateTime.now().minusDays(2)) + .build(); + Users deletedPartner = UserTestFixture.createPersonalPartner().toBuilder() + .userUuid(UUID.randomUUID()) + .deletedAt(LocalDateTime.now().minusDays(1)) + .build(); + + fakeUserQueryRepository.addUser(admin); + fakeUserQueryRepository.addUser(deletedUser); + fakeUserQueryRepository.addUser(deletedPartner); + + GetWithdrawUserListInDto inDto = GetWithdrawUserListInDto.builder() + .requestorUuid(admin.getUserUuid()) + .userTypeList(List.of(UserType.PERSONAL_PARTNER)) + .pageable(PageRequest.of(0, 10)) + .build(); + + // when + WithdrawUserListOutDto result = getUserDataService.getWithdrawUserList(inDto); + + // then + assertThat(result.getContent()).hasSize(1); + assertThat(result.getContent().get(0).getUserType()).isEqualTo(UserType.PERSONAL_PARTNER.getDescription()); + } + } + +} diff --git a/src/test/java/greenfirst/be/users/fake/FakeUserQueryRepository.java b/src/test/java/greenfirst/be/users/fake/FakeUserQueryRepository.java index d38b754..65a4dea 100644 --- a/src/test/java/greenfirst/be/users/fake/FakeUserQueryRepository.java +++ b/src/test/java/greenfirst/be/users/fake/FakeUserQueryRepository.java @@ -4,13 +4,17 @@ import greenfirst.be.global.common.enums.common.UserType; import greenfirst.be.user.adapter.out.persistence.entity.vo.BranchOptionVo; import greenfirst.be.user.application.dto.in.GetUserSimpleDataListInDto; +import greenfirst.be.user.application.dto.in.GetWithdrawUserListInDto; import greenfirst.be.user.application.dto.out.UserDataListOutDto; import greenfirst.be.user.application.dto.out.UserSimpleDataOutDto; +import greenfirst.be.user.application.dto.out.WithdrawUserListOutDto; +import greenfirst.be.user.application.dto.out.WithdrawUserSimpleDataOutDto; import greenfirst.be.user.application.port.out.repository.UserQueryRepository; import greenfirst.be.user.domain.model.Users; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; +import java.time.LocalDateTime; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -126,6 +130,59 @@ public UserDataListOutDto getUserSimpleDataList(GetUserSimpleDataListInDto inDto .build(); } + @Override + public WithdrawUserListOutDto getWithdrawUserList(GetWithdrawUserListInDto inDto) { + + // 전체 유저 목록 가져오기 + List allUsers = storage.values().stream().collect(Collectors.toList()); + + // 필터링 (탈퇴 사용자만) + List filteredUsers = allUsers.stream() + .filter(user -> user.getDeletedAt() != null) + .filter(user -> inDto.getUserName() == null || user.getUserName().contains(inDto.getUserName())) + .filter(user -> inDto.getUserPhoneNumber() == null || user.getUserPhoneNumber().contains(inDto.getUserPhoneNumber())) + .filter(user -> inDto.getUserTypeList() == null || inDto.getUserTypeList().isEmpty() || inDto.getUserTypeList().contains(user.getUserType())) + .sorted((a, b) -> { + LocalDateTime aDeletedAt = a.getDeletedAt(); + LocalDateTime bDeletedAt = b.getDeletedAt(); + boolean isDesc = inDto.getOrderType() == null || inDto.getOrderType().isBlank() || "DESC".equalsIgnoreCase(inDto.getOrderType()); + return isDesc ? bDeletedAt.compareTo(aDeletedAt) : aDeletedAt.compareTo(bDeletedAt); + }) + .collect(Collectors.toList()); + + // 페이징 처리 + Pageable pageable = inDto.getPageable() != null ? inDto.getPageable() : PageRequest.of(0, 10); + int start = (int) pageable.getOffset(); + int end = Math.min(start + pageable.getPageSize(), filteredUsers.size()); + + List pagedUsers = start >= filteredUsers.size() + ? List.of() + : filteredUsers.subList(start, end); + + // DTO 변환 + List content = pagedUsers.stream() + .map(user -> new WithdrawUserSimpleDataOutDto( + user.getUserUuid(), + user.getUserType(), + user.getUserName(), + user.getUserPhoneNumber(), + user.getUserLoginId(), + user.getDeletedAt() + )) + .collect(Collectors.toList()); + + long totalElements = filteredUsers.size(); + long totalPages = (long) Math.ceil((double) totalElements / pageable.getPageSize()); + + return WithdrawUserListOutDto.builder() + .content(content) + .totalElementCount(totalElements) + .totalPageCount(totalPages) + .number(pageable.getPageNumber() + 1) + .size(pageable.getPageSize()) + .build(); + } + @Override public boolean existsByLoginId(String userLoginId) {