diff --git a/src/main/java/com/bmilab/backend/domain/user/controller/AdminUserApi.java b/src/main/java/com/bmilab/backend/domain/user/controller/AdminUserApi.java index 817b9c8e..6b514751 100644 --- a/src/main/java/com/bmilab/backend/domain/user/controller/AdminUserApi.java +++ b/src/main/java/com/bmilab/backend/domain/user/controller/AdminUserApi.java @@ -2,6 +2,7 @@ import com.bmilab.backend.domain.user.dto.request.RegisterUserRequest; import com.bmilab.backend.domain.user.dto.request.AdminUpdateUserRequest; +import com.bmilab.backend.domain.user.dto.request.UpdateUserStatusRequest; import com.bmilab.backend.domain.user.dto.request.UserAccountEmailRequest; import com.bmilab.backend.domain.user.dto.response.UserDetail; import com.bmilab.backend.global.exception.ErrorResponse; @@ -95,4 +96,49 @@ ResponseEntity sendAccountEmail( @PathVariable Long userId, @RequestBody UserAccountEmailRequest request ); + + @Operation(summary = "사용자 퇴사 처리", description = "사용자를 퇴사 처리하는 PATCH API. 퇴사 처리된 사용자는 로그인할 수 없습니다.") + @ApiResponses( + value = { + @ApiResponse( + responseCode = "200", + description = "퇴사 처리 성공" + ), + @ApiResponse( + responseCode = "400", + description = "관리자 계정은 퇴사 처리할 수 없습니다.", + content = @Content(schema = @Schema(implementation = ErrorResponse.class)) + ), + @ApiResponse( + responseCode = "404", + description = "사용자 정보를 찾을 수 없습니다.", + content = @Content(schema = @Schema(implementation = ErrorResponse.class)) + ) + } + ) + ResponseEntity resignUser(@PathVariable Long userId); + + @Operation(summary = "사용자 상태 변경", description = "사용자의 상태(재직/휴직/퇴사)를 변경하는 PATCH API") + @ApiResponses( + value = { + @ApiResponse( + responseCode = "200", + description = "상태 변경 성공" + ), + @ApiResponse( + responseCode = "400", + description = "관리자 계정은 퇴사 처리할 수 없습니다.", + content = @Content(schema = @Schema(implementation = ErrorResponse.class)) + ), + @ApiResponse( + responseCode = "404", + description = "사용자 정보를 찾을 수 없습니다.", + content = @Content(schema = @Schema(implementation = ErrorResponse.class)) + ) + } + ) + ResponseEntity updateUserStatus( + @PathVariable Long userId, + @RequestBody UpdateUserStatusRequest request + ); } diff --git a/src/main/java/com/bmilab/backend/domain/user/controller/AdminUserController.java b/src/main/java/com/bmilab/backend/domain/user/controller/AdminUserController.java index 66db8581..068f321e 100644 --- a/src/main/java/com/bmilab/backend/domain/user/controller/AdminUserController.java +++ b/src/main/java/com/bmilab/backend/domain/user/controller/AdminUserController.java @@ -2,6 +2,7 @@ import com.bmilab.backend.domain.user.dto.request.RegisterUserRequest; import com.bmilab.backend.domain.user.dto.request.AdminUpdateUserRequest; +import com.bmilab.backend.domain.user.dto.request.UpdateUserStatusRequest; import com.bmilab.backend.domain.user.dto.request.UserAccountEmailRequest; import com.bmilab.backend.domain.user.dto.response.UserDetail; import com.bmilab.backend.domain.user.service.AuthService; @@ -67,4 +68,21 @@ public ResponseEntity sendAccountEmail( userService.sendAccountEmail(userId, request); return ResponseEntity.ok().build(); } + + @PatchMapping("/{userId}/resign") + public ResponseEntity resignUser(@PathVariable Long userId) { + + userService.resignUser(userId); + return ResponseEntity.ok().build(); + } + + @PatchMapping("/{userId}/status") + public ResponseEntity updateUserStatus( + @PathVariable Long userId, + @RequestBody @Valid UpdateUserStatusRequest request + ) { + + userService.updateUserStatus(userId, request.status()); + return ResponseEntity.ok().build(); + } } diff --git a/src/main/java/com/bmilab/backend/domain/user/dto/request/UpdateUserStatusRequest.java b/src/main/java/com/bmilab/backend/domain/user/dto/request/UpdateUserStatusRequest.java new file mode 100644 index 00000000..59ecb49d --- /dev/null +++ b/src/main/java/com/bmilab/backend/domain/user/dto/request/UpdateUserStatusRequest.java @@ -0,0 +1,12 @@ +package com.bmilab.backend.domain.user.dto.request; + +import com.bmilab.backend.domain.user.enums.UserStatus; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotNull; + +public record UpdateUserStatusRequest( + @Schema(description = "사용자 상태", example = "RESIGNED") + @NotNull(message = "사용자 상태는 필수입니다.") + UserStatus status +) { +} \ No newline at end of file diff --git a/src/main/java/com/bmilab/backend/domain/user/entity/User.java b/src/main/java/com/bmilab/backend/domain/user/entity/User.java index 58881fc9..a84cb2c4 100644 --- a/src/main/java/com/bmilab/backend/domain/user/entity/User.java +++ b/src/main/java/com/bmilab/backend/domain/user/entity/User.java @@ -1,7 +1,9 @@ package com.bmilab.backend.domain.user.entity; import com.bmilab.backend.domain.user.enums.UserPosition; +import com.bmilab.backend.domain.user.enums.UserStatus; import com.bmilab.backend.global.entity.BaseTimeEntity; +import java.time.LocalDateTime; import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.EnumType; @@ -52,6 +54,13 @@ public class User extends BaseTimeEntity { @Enumerated(EnumType.STRING) private Role role; + @Enumerated(EnumType.STRING) + @Column(nullable = false) + @Builder.Default + private UserStatus status = UserStatus.ACTIVE; + + @Column(name = "resigned_at") + private LocalDateTime resignedAt; public void updatePassword(String newPassword) { this.password = newPassword; @@ -76,4 +85,20 @@ public void updateRole(Role role) { public boolean isAdmin() { return Role.ADMIN.equals(role); } + + public boolean isActive() { + return UserStatus.ACTIVE.equals(status); + } + + public void resign() { + this.status = UserStatus.RESIGNED; + this.resignedAt = LocalDateTime.now(); + } + + public void updateStatus(UserStatus status) { + this.status = status; + if (status == UserStatus.RESIGNED) { + this.resignedAt = LocalDateTime.now(); + } + } } diff --git a/src/main/java/com/bmilab/backend/domain/user/enums/UserStatus.java b/src/main/java/com/bmilab/backend/domain/user/enums/UserStatus.java new file mode 100644 index 00000000..cf04f9d9 --- /dev/null +++ b/src/main/java/com/bmilab/backend/domain/user/enums/UserStatus.java @@ -0,0 +1,14 @@ +package com.bmilab.backend.domain.user.enums; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public enum UserStatus { + ACTIVE("재직"), + ON_LEAVE("휴직"), + RESIGNED("퇴사"); + + private final String description; +} \ No newline at end of file diff --git a/src/main/java/com/bmilab/backend/domain/user/exception/UserErrorCode.java b/src/main/java/com/bmilab/backend/domain/user/exception/UserErrorCode.java index 430d17cb..99255d46 100644 --- a/src/main/java/com/bmilab/backend/domain/user/exception/UserErrorCode.java +++ b/src/main/java/com/bmilab/backend/domain/user/exception/UserErrorCode.java @@ -15,6 +15,8 @@ public enum UserErrorCode implements ErrorCode { SAME_AS_CURRENT_PASSWORD("새 비밀번호가 현재 비밀번호와 같습니다.", HttpStatus.BAD_REQUEST), INVALID_PROFILE_IMAGE_FILE_TYPE("프로필 이미지의 파일 형식이 잘못되었습니다.", HttpStatus.BAD_REQUEST), DUPLICATE_EMAIL("이미 가입된 이메일입니다.", HttpStatus.CONFLICT), + USER_NOT_ACTIVE("비활성화된 계정입니다.", HttpStatus.FORBIDDEN), + CANNOT_RESIGN_ADMIN("관리자 계정은 퇴사 처리할 수 없습니다.", HttpStatus.BAD_REQUEST), ; private final String message; diff --git a/src/main/java/com/bmilab/backend/domain/user/service/UserService.java b/src/main/java/com/bmilab/backend/domain/user/service/UserService.java index 885deed7..092818e8 100644 --- a/src/main/java/com/bmilab/backend/domain/user/service/UserService.java +++ b/src/main/java/com/bmilab/backend/domain/user/service/UserService.java @@ -19,6 +19,7 @@ import com.bmilab.backend.domain.user.entity.UserInfo; import com.bmilab.backend.domain.user.entity.UserProjectCategory; import com.bmilab.backend.domain.user.entity.UserSubAffiliation; +import com.bmilab.backend.domain.user.enums.UserStatus; import com.bmilab.backend.domain.user.event.UserEducationUpdateEvent; import com.bmilab.backend.domain.user.exception.UserErrorCode; import com.bmilab.backend.domain.user.repository.UserEducationRepository; @@ -287,6 +288,30 @@ public void deleteUserById(Long userId) { userRepository.deleteById(userId); } + @Transactional + public void resignUser(Long userId) { + + User user = findUserById(userId); + + if (user.isAdmin()) { + throw new ApiException(UserErrorCode.CANNOT_RESIGN_ADMIN); + } + + user.resign(); + } + + @Transactional + public void updateUserStatus(Long userId, UserStatus status) { + + User user = findUserById(userId); + + if (user.isAdmin() && status == UserStatus.RESIGNED) { + throw new ApiException(UserErrorCode.CANNOT_RESIGN_ADMIN); + } + + user.updateStatus(status); + } + public SearchUserResponse searchUsers(String keyword) { List users = userRepository.searchUsersByKeyword(keyword); diff --git a/src/main/java/com/bmilab/backend/global/security/UserAuthInfoService.java b/src/main/java/com/bmilab/backend/global/security/UserAuthInfoService.java index 1e4bb71f..e58abb26 100644 --- a/src/main/java/com/bmilab/backend/global/security/UserAuthInfoService.java +++ b/src/main/java/com/bmilab/backend/global/security/UserAuthInfoService.java @@ -1,6 +1,7 @@ package com.bmilab.backend.global.security; import com.bmilab.backend.domain.user.entity.User; +import com.bmilab.backend.domain.user.exception.UserErrorCode; import com.bmilab.backend.domain.user.repository.UserRepository; import com.bmilab.backend.global.exception.ApiException; import com.bmilab.backend.global.exception.GlobalErrorCode; @@ -21,6 +22,10 @@ public UserDetails loadUserByUsername(String email) { User user = userRepository.findByEmail(email) .orElseThrow(() -> new ApiException(GlobalErrorCode.SECURITY_USER_NOT_FOUND)); + if (!user.isActive()) { + throw new ApiException(UserErrorCode.USER_NOT_ACTIVE); + } + return new UserAuthInfo(user, email); } catch(UsernameNotFoundException exception) { throw new ApiException(GlobalErrorCode.SECURITY_USER_NOT_FOUND, exception);