개발 기록/도서관 관리 시스템

[Library Management System] 25.09.30 (56일) | (리팩토링) 관리자 사용자 전체 목록 조회, 타입별 회원 검색, 회원 권한 수정, 회원 상태 수정, 탈퇴 처리

dev.jelee 2025. 9. 30. 12:29

[ 작업한 내용 ]

< 관리자: 사용자 전체 목록 조회 >

# AdminUserListResDTO

- UserListResDTO -> AdminUserListResDTO 네이밍 변경

- 사용자 고유번호, 아이디, 이메일, 권한, 가입날짜, 마지막 접속날짜, 상태 (id, username, email, role, joinDate, lastLoginDate, status)

* 필드는 그대로, 네이밍만 변경

@Getter
public class AdminUserListResDTO {
  private long id;
  private String username;
  private String email;
  private Role role;
  private LocalDateTime joinDate;
  private LocalDateTime lastLoginDate;
  private UserStatus status;

  public AdminUserListResDTO(User user) {
    this.id = user.getId();
    this.username = user.getUsername();
    this.email = user.getEmail();
    this.role = user.getRole();
    this.joinDate = user.getJoinDate();
    this.lastLoginDate = user.getLastLoginDate();
    this.status = user.getStatus();
  }
}

# AdminUserController

- GET /api/v1/admin/users 로 요청

- page, size, @AuthenticationPrincipal User user 파라미터

- Service 계층으로 page, size, user.getId()를 전달

- 클라이언트 측으로 성공 메시지와 반환 결과를 ApiResponse.seuccess()로 감싸서 응답

* 관리자 권한 확인을 위해 @AuthenticationPrincipal을 추가. 인증 객체는 커스텀한 User 엔티티로 생성.

@GetMapping()
public ResponseEntity<?> allListUsers(
  @RequestParam(name = "page", defaultValue = "0") int page,
  @RequestParam(name = "size", defaultValue = "10") int size,
  @AuthenticationPrincipal User user) {
  
  // 서비스로직
  Page<AdminUserListResDTO> responseDTO = adminUserService.allListUsers(page, size, user.getId());

  // 성공 메시지
  String message = messageProvider.getMessage(UserSuccessCode.USER_LIST_FETCHED.getMessage());

  // 응답
  return ResponseEntity
            .status(UserSuccessCode.USER_LIST_FETCHED.getHttpStatus())
            .body(ApiResponse.success(
              UserSuccessCode.USER_LIST_FETCHED, 
              message, 
              responseDTO));
}

# AdminUserService

- UserService에 통합으로 사용자, 관리자 기능이 작성되어 있는데 관리자 기능은 AdminUserService로 옮김.

- userRepository.findById(userId)로 유효한 사용자인지 조회를 하고 유효하지 않으면 UserErrorCode.USER_NOT_FOUND 예외를 발생.

- user.getRole() != Role.ROLE_ADMIN 로 권한을 체크한 후 관리자 권한이 아니라면 AuthErrorCode.AUTH_FORBIDDEN 예외를 발생.

- PageRequest.of(page, size)로 페이징 정보를 정의하고 Pageable형태로 저장.

- userRepository.findAll(pageable)로 사용자 정보를 정의한 페이징 설정으로 조회하여 Page<User> 형태로 저장.

- 클라이언트 측으로 반환을 할 때에는 User 엔티티 그대로 정보를 전달하면 안되서 Page<AdminUserListResDTO> 형태로 반환.

public Page<AdminUserListResDTO> allListUsers(int page, int size, Long userId) {
  
  // 관리자 권환 조회 및 예외 처리
  User user = userRepository.findById(userId)
      .orElseThrow(() -> new BaseException(UserErrorCode.USER_NOT_FOUND));
  
  if (user.getRole() != Role.ROLE_ADMIN) {
    throw new BaseException(AuthErrorCode.AUTH_FORBIDDEN);
  }

  // 페이징 정의
  Pageable pageable = PageRequest.of(page, size);

  // Page형태로 사용자 조회
  Page<User> result = userRepository.findAll(pageable);

  // User -> AdminUserListResDTO로 맵핑
  Page<AdminUserListResDTO> pageDTO = result.map(AdminUserListResDTO::new);

  // 반환
  return pageDTO;
}

# UserRepository

- 굳이 재정의할 필요가 없는 public findAll(Pageable pageable); 을 삭제


< 관리자: 회원 검색 >

# AdminUserSearchResDTO

- UserSearchResDTO -> AdminUserSearchResDTO 네이밍 변경

- 사용자 고유번호, 아이디, 이메일, 권한, 가입날짜, 마지막 접속날짜, 상태 (id, username, email, role, joinDate, lastLoginDate, status)

- ** 필드는 그대로, 네이밍만 변경

@Getter
public class AdminUserSearchResDTO {
  private long id;
  private String username;
  private String email;
  private Role role;
  private LocalDateTime joinDate;
  private LocalDateTime lastLoginDate;
  private UserStatus status;

  public AdminUserSearchResDTO(User user) {
    this.id = user.getId();
    this.username = user.getUsername();
    this.email = user.getEmail();
    this.role = user.getRole();
    this.joinDate = user.getJoinDate();
    this.lastLoginDate = user.getLastLoginDate();
    this.status = user.getStatus();
  }
}

# AdminUserController

- GET /api/v1/admin/users/search 로 요청

- type, keyword, page, size, @AuthenticationPrincipal User user 파라미터

- Service 계층으로 type, keyword, page, size, user.getId()를 전달

- 클라이언트 측으로 성공 메시지와 반환 결과를 ApiResponse.seuccess()로 감싸서 응답

* 관리자 권한 확인을 위해 @AuthenticationPrincipal을 추가. 인증 객체는 커스텀한 User 엔티티로 생성.

@GetMapping("search")
public ResponseEntity<?> searchUser(
  @RequestParam("type") UserSearchType type,
  @RequestParam("keyword") String keyword,
  @RequestParam(value = "page", defaultValue = "0") int page,
  @RequestParam(value = "size", defaultValue = "10") int size,
  @AuthenticationPrincipal User user) {
    
    // 서비스로직
    Page<AdminUserSearchResDTO> responseDTO = adminUserService.searchUser(type, keyword, page, size, user.getId());

    // 성공메시지
    String message = messageProvider.getMessage(UserSuccessCode.USER_FETCHED.getMessage());

    // 응답
    return ResponseEntity
              .status(UserSuccessCode.USER_ACCOUNT_DELETED.getHttpStatus())
              .body(ApiResponse.success(
                UserSuccessCode.USER_LIST_FETCHED, 
                message, 
                responseDTO));    
}

# AdminUserService

- UserService에 통합으로 사용자, 관리자 기능이 작성되어 있는데 관리자 기능은 AdminUserService로 옮김.

- userRepository.findById(userId)로 유효한 사용자인지 조회를 하고 유효하지 않으면 UserErrorCode.USER_NOT_FOUND 예외를 발생.

- user.getRole() != Role.ROLE_ADMIN 로 권한을 체크한 후 관리자 권한이 아니라면 AuthErrorCode.AUTH_FORBIDDEN 예외를 발생.

- PageRequest.of(page, size)로 페이징 정보를 정의하고 Pageable형태로 저장.

- 타입별 Page형태로 사용자 검색 조회하기 위해 UserSearchType type, Page<User> result를 생성.

- switch문을 사용하여 type이 USERNAME, EMAIL 인지 case를 체크하여 각 메서드 실행. 

- 클라이언트 측으로 반환을 할 때에는 User 엔티티 그대로 정보를 전달하면 안되서 Page<AdminUserSearchResDTO> 형태로 반환.

public Page<AdminUserSearchResDTO> searchUser(UserSearchType typeStr, String keyword, int page, int size, Long userId) { 
  
  // 관리자 권한 조회 및 예외 처리
  User user = userRepository.findById(userId)
      .orElseThrow(() -> new BaseException(UserErrorCode.USER_NOT_FOUND));

  if (user.getRole() != Role.ROLE_ADMIN) {
    throw new BaseException(AuthErrorCode.AUTH_FORBIDDEN);
  }

  // 페이징 정의
  Pageable pageable = PageRequest.of(page, size);

  // 타입별 Page형태로 사용자 검색 조회
  UserSearchType type = typeStr;
  Page<User> result;

  switch(type) {
    case USERNAME:
      result = userRepository.findByUsernameContainingIgnoreCase(keyword, pageable);
      break;
    case EMAIL:
      result = userRepository.findByEmailContainingIgnoreCase(keyword, pageable);
      break;
    default:
      throw new IllegalArgumentException("올바른 타입을 선택해주세요 (USERNAME, EMAIL): " + type);
  }

  // User -> AdminUserSearchResDTO로 맵핑
  Page<AdminUserSearchResDTO> pageDTO = result.map(AdminUserSearchResDTO::new);

  // 반환
  return pageDTO;
}

# UserRepository

- USERNAME 타입으로 검색시 페이징으로 조회하는 메서드 정의.

- EMAIL 타입으로 검색시 페이징으로 조회하는 메서드 정의.

- ** 주석만 수정

  Page<User> findByUsernameContainingIgnoreCase(String username, Pageable pageable);
  Page<User> findByEmailContainingIgnoreCase(String email, Pageable pageable);

< 관리자: 회원 권한 수정 >

# AdminUserRoleUpdateReqDTO

- UserRoleUpdateReqDTO -> AdminUserRoleUpdateReqDTO 네이밍 변경.

- 권한 (role)

@Getter
public class AdminUserRoleUpdateReqDTO {
  private Role role;
}

# AdminUserRoleUpdateResDTO

- 사용자 고유번호, 아이디, 권한, 수정날짜 (id, username, role, updatedAt)

- User엔티티를 파라미터로 받는 생성자 함수 정의.

** 생성자함수 추가함.

@Getter
public class AdminUserRoleUpdatedResDTO {
  private Long id;
  private String username;
  private Role role;
  private LocalDateTime updatedAt;

  public AdminUserRoleUpdatedResDTO(User user) {
    this.id = user.getId();
    this.username = user.getUsername();
    this.role = user.getRole();
    this.updatedAt = user.getUpdatedAt();
  }
}

# AdminUserController

- PATCH /api/v1/admin/users/{userId}/role 로 요청

- userId, AdminUserRoleUpdateReqDTO requestDTO, @AuthenticationPrincipal User user 파라미터

- Service 계층으로 userId, requestDTO, user.getId()를 전달

- 클라이언트 측으로 성공 메시지와 반환 결과를 ApiResponse.seuccess()로 감싸서 응답

** 관리자 권한 확인을 위해 @AuthenticationPrincipal을 추가. 인증 객체는 커스텀한 User 엔티티로 생성.

@PatchMapping("/{userId}/role")
public ResponseEntity<?> updateUserRole(
  @PathVariable("userId") Long userId,
  @RequestBody AdminUserRoleUpdateReqDTO requestDTO,
  @AuthenticationPrincipal User user) {
  
    // 서비스로직
    AdminUserRoleUpdatedResDTO responseDTO = adminUserService.updateUserRole(userId, requestDTO, user.getId());

    // 성공메시지
    String message = messageProvider.getMessage(UserSuccessCode.USER_ROLE_UPDATED.getMessage());

    // 응답
    return ResponseEntity
              .status(UserSuccessCode.USER_ROLE_UPDATED.getHttpStatus())
              .body(ApiResponse.success(
                UserSuccessCode.USER_ROLE_UPDATED, 
                message, 
                responseDTO));
}

# AdminUserService

- UserService에 통합으로 사용자, 관리자 기능이 작성되어 있는데 관리자 기능은 AdminUserService로 옮김.

- 관리자 권한 확인을 위해 userRepository.findById(amdinUserId)로 유효한 사용자인지 조회를 하고 유효하지 않으면 UserErrorCode.USER_NOT_FOUND 예외를 발생.

- user.getRole() != Role.ROLE_ADMIN 로 권한을 체크한 후 관리자 권한이 아니라면 AuthErrorCode.AUTH_FORBIDDEN 예외를 발생.

- 권한 수정할 사용자의 유효성 체크를 위해 userRepository.findById(userId)로 조회하고 없다면 UserErrorCode.USER_NOT_FOUND, "userId: " + userId 예외를 발생.

- User 객체에 변경할 권한과 수정된 날짜를 저장 후 userRepository.save(user)를 사용하여 DB에 업데이트 해주기.

- 클라이언트 측으로 반환을 할 때에는 User 기반으로 AdminUserRoleUpdatedResDTO 객체를 생성하여 반환

@Transactional
public AdminUserRoleUpdatedResDTO updateUserRole(Long userId, AdminUserRoleUpdateReqDTO roleUpdateDTO, Long adminUserId) {

  // 관리자 권한 조회 및 예외 처리
  User userAdmin = userRepository.findById(adminUserId)
      .orElseThrow(() -> new BaseException(UserErrorCode.USER_NOT_FOUND));
  
  if (userAdmin.getRole() != Role.ROLE_ADMIN) {
    throw new BaseException(AuthErrorCode.AUTH_FORBIDDEN);
  }

  // 사용자 정보 확인 및 예외 처리
  User user = userRepository.findById(userId)
      .orElseThrow(() -> new BaseException(UserErrorCode.USER_NOT_FOUND, "userId: " + userId));
  
  // 권한 변경 및 저장
  user.setRole(roleUpdateDTO.getRole());
  user.setUpdatedAt(LocalDateTime.now());
  userRepository.save(user);

  // 반환
  return new AdminUserRoleUpdatedResDTO(user);
}

# messages.properties

- 메시지 정리하면서 발생한 오탈자 수정.

success.user.role_updated=권한이 성공적으로 변경되었습니다.

< 관리자: 사용자 상태 수정 >

# AdminUserStatusUpdateReqDTO

- UserStatusUpdateReqDTO -> AdminUserStatusUpdateReqDTO 네이밍 변경.

- 상태 (status)

@Getter
public class AdminUserStatusUpdateReqDTO {
  private UserStatus status;
}

# AdminUserStatusUpdateResDTO

- 사용자 고유번호, 아이디, 상태, 수정날짜 (id, username, status, updatedAt)

- User엔티티를 파라미터로 받는 생성자 함수 정의.

* 생성자함수 추가함.

@Getter
public class AdminUserStatusUpdateResDTO {
  private Long id;
  private String username;
  private UserStatus status;
  private LocalDateTime updatedAt;

  public AdminUserStatusUpdateResDTO(User user) {
    this.id = user.getId();
    this.username = user.getUsername();
    this.status = user.getStatus();
    this.updatedAt = user.getUpdatedAt();
  }
}

# AdminUserController

- PATCH /api/v1/admin/users/{userId}/status 로 요청

- userId, AdminUserStatusUpdateReqDTO requestDTO, @AuthenticationPrincipal User user 파라미터

- Service 계층으로 userId, requestDTO, user.getId()를 전달

- 클라이언트 측으로 성공 메시지와 반환 결과를 ApiResponse.seuccess()로 감싸서 응답

* 관리자 권한 확인을 위해 @AuthenticationPrincipal을 추가. 인증 객체는 커스텀한 User 엔티티로 생성.

@PatchMapping("/{userId}/status")
public ResponseEntity<?> updateUserStatus(
  @PathVariable("userId") Long userId, 
  @RequestBody AdminUserStatusUpdateReqDTO requestDTO,
  @AuthenticationPrincipal User user) {
  
    // 서비스로직
    AdminUserStatusUpdateResDTO responseDTO = adminUserService.updateUserStatus(userId, requestDTO, user.getId());
    
    // 성공메시지
    String message = messageProvider.getMessage(UserSuccessCode.USER_STATUS_UPDATED.getMessage());

    // 응답
    return ResponseEntity
              .status(UserSuccessCode.USER_STATUS_UPDATED.getHttpStatus())
              .body(ApiResponse.success(
                UserSuccessCode.USER_STATUS_UPDATED, 
                message, 
                responseDTO));
}

# AdminUserService

- UserService에 통합으로 사용자, 관리자 기능이 작성되어 있는데 관리자 기능은 AdminUserService로 옮김.

- 관리자 권한 확인을 위해 userRepository.findById(amdinUserId)로 유효한 사용자인지 조회를 하고 유효하지 않

다면 UserErrorCode.USER_NOT_FOUND 예외를 발생.

- user.getRole() != Role.ROLE_ADMIN 로 권한을 체크한 후 관리자 권한이 아니라면 AuthErrorCode.AUTH_FORBIDDEN 예외를 발생.

- 상태 수정할 사용자의 유효성 체크를 위해 userRepository.findById(userId)로 조회하고 없다면 UserErrorCode.USER_NOT_FOUND, "userId: " + userId 예외를 발생.

- User 객체에 변경할 상태와 수정된 날짜를 저장 후 userRepository.save(user)를 사용하여 DB에 업데이트 해주기.

- 클라이언트 측으로 반환을 할 때에는 User 기반으로 AdminUserStatusUpdateResDTO 객체를 생성하여 반환

@Transactional
public AdminUserStatusUpdateResDTO updateUserStatus(Long userId, AdminUserStatusUpdateReqDTO requestDTO, Long adminUserId) {

  // 관리자 권한 조회 및 예외 처리
  User userAdmin = userRepository.findById(adminUserId)
      .orElseThrow(() -> new BaseException(UserErrorCode.USER_NOT_FOUND));
  
  if (userAdmin.getRole() != Role.ROLE_ADMIN) {
    throw new BaseException(AuthErrorCode.AUTH_FORBIDDEN);
  }

  // 사용자 정보 확인 및 예외 처리
  User user = userRepository.findById(userId)
      .orElseThrow(() -> new BaseException(UserErrorCode.USER_NOT_FOUND, "userId: " + userId));
  
  // 상태 변경 및 저장
  user.setStatus(requestDTO.getStatus());
  user.setUpdatedAt(LocalDateTime.now());
  userRepository.save(user);

  // 반환
  return new AdminUserStatusUpdateResDTO(user);
}

# UserStatus enum

- enum에 주석 달아줌.

public enum UserStatus {
  ACTIVE,     // 활성화
  INACTIVE,   // 비활성화
  SUSPENDED,  // 정지
  DELETED     // 삭제된 상태
}

< 관리자: 회원 탈퇴 처리 >

# AdminUserDeleteResDTO

- UserDeleteResDTO -> AdminUserDeleteResDTO 네이밍 변경.

- 사용자 고유번호, 아이디, 이메일, 수정날짜, 상태 (id, username, email, updatedAt, status)

- User 엔티티를 파라미터로 받는 생성자 함수 정의.

** 생성자 함수 추가.

@Getter
public class AdminUserDeleteResDTO {
  private Long id;
  private String username;
  private String email;
  private LocalDateTime updatedAt;
  private UserStatus status;

  public AdminUserDeleteResDTO(User user) {
    this.id = user.getId();
    this.username = user.getUsername();
    this.email = user.getEmail();
    this.updatedAt = user.getUpdatedAt();
    this.status = user.getStatus();
  }
}

# AdminUserController

- PATCH /api/v1/admin/users/{userId}/deactivate 로 요청

- userId, @AuthenticationPrincipal User user 파라미터

- Service 계층으로 userId, user.getId()를 전달

- 클라이언트 측으로 성공 메시지와 반환 결과를 ApiResponse.seuccess()로 감싸서 응답

** 관리자 권한 확인을 위해 @AuthenticationPrincipal을 추가. 인증 객체는 커스텀한 User 엔티티로 생성.

@PatchMapping("/{userId}/deactivate")
public ResponseEntity<?> deleteUserAccount(
  @PathVariable("userId") Long userId, 
  @AuthenticationPrincipal User user) {
  
    // 서비스로직
    AdminUserDeleteResDTO responseDTO = adminUserService.deleteUserAccount(userId, user.getId());
    
    // 성공메시지
    String message = messageProvider.getMessage(UserSuccessCode.USER_ACCOUNT_DELETED.getMessage());

    // 응답
    return ResponseEntity
              .status(UserSuccessCode.USER_ACCOUNT_DELETED.getHttpStatus())
              .body(ApiResponse.success(
                UserSuccessCode.USER_ACCOUNT_DELETED, 
                message, 
                responseDTO));
}

# AdminUserService

- UserService에 통합으로 사용자, 관리자 기능이 작성되어 있는데 관리자 기능은 AdminUserService로 옮김.

- 관리자 권한 확인을 위해 userRepository.findById(amdinUserId)로 유효한 사용자인지 조회를 하고 유효하지 않으면 UserErrorCode.USER_NOT_FOUND 예외를 발생.

- user.getRole() != Role.ROLE_ADMIN 로 권한을 체크한 후 관리자 권한이 아니라면 AuthErrorCode.AUTH_FORBIDDEN 예외를 발생.

- 상태 수정할 사용자의 유효성 체크를 위해 userRepository.findById(userId)로 조회하고 없다면 UserErrorCode.USER_NOT_FOUND, "userId: " + userId 예외를 발생.

- 사용자 상태가 DELETED면 username과 email 뒤에 "_deleted_ + 삭제날짜"를 추가한 뒤, userRepository.save(user)를 사용해 DB에 저장한다. 상태가 DELETED가 아니리면 UserErrorCode.USER_STATUS_NOT_DELETED 예외를 발생시킨다.

- 클라이언트 측으로 반환을 할 때에는 User 기반으로 AdminUserDeleteResDTO 객체를 생성하여 반환

@Transactional
public AdminUserDeleteResDTO deleteUserAccount(Long userId, Long adminUserId) {

  // 관리자 권한 조회 및 예외 처리
  User userAdmin = userRepository.findById(adminUserId)
      .orElseThrow(() -> new BaseException(UserErrorCode.USER_NOT_FOUND, "userId: " + userId));
  
  if (userAdmin.getRole() != Role.ROLE_ADMIN) {
    throw new BaseException(AuthErrorCode.AUTH_FORBIDDEN);
  }

  // 사용자 정보 확인 및 예외 처리
  User user = userRepository.findById(userId)
      .orElseThrow(() -> new BaseException(UserErrorCode.USER_NOT_FOUND));

  // 사용자 상태가 DELETED 이면 username, email 수정 (_deleted_+삭제날짜)
  if (user.getStatus() == UserStatus.DELETED) {
    String today = LocalDate.now().format(DateTimeFormatter.BASIC_ISO_DATE); // e.g., 20250930
    String userEmail = user.getEmail();
    user.setUsername(user.getUsername() + "_deleted_" + today);
    user.setEmail(userEmail.substring(0, userEmail.indexOf("@")) + "_deleted_" + today + "@deleted.local");
    userRepository.save(user);
  } else {
    throw new BaseException(UserErrorCode.USER_STATUS_NOT_DELETED);
  }
  
  // 반환
  return new AdminUserDeleteResDTO(user);
}

commit
회원 전체 목록 조회 성공(좌), 회원 검색 조회 성공(우)
회원 권한 수정 성공(좌), 회원 상태 수정 성공(가운데), 회원 탈퇴 처리 성공(우)