diff --git a/src/main/java/project/flipnote/common/entity/SoftDeletableEntity.java b/src/main/java/project/flipnote/common/entity/SoftDeletableEntity.java index 9c295b69..9963b4df 100644 --- a/src/main/java/project/flipnote/common/entity/SoftDeletableEntity.java +++ b/src/main/java/project/flipnote/common/entity/SoftDeletableEntity.java @@ -9,13 +9,13 @@ @MappedSuperclass public abstract class SoftDeletableEntity extends BaseEntity { - private LocalDateTime deletedDate; + private LocalDateTime deletedAt; public void softDelete() { - this.deletedDate = LocalDateTime.now(); + this.deletedAt = LocalDateTime.now(); } public boolean isDeleted() { - return deletedDate != null; + return deletedAt != null; } } diff --git a/src/main/java/project/flipnote/user/controller/UserController.java b/src/main/java/project/flipnote/user/controller/UserController.java index a98e67ee..4a62aef8 100644 --- a/src/main/java/project/flipnote/user/controller/UserController.java +++ b/src/main/java/project/flipnote/user/controller/UserController.java @@ -2,6 +2,8 @@ import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; @@ -9,6 +11,7 @@ import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; +import project.flipnote.common.security.dto.UserAuth; import project.flipnote.user.model.UserRegisterRequest; import project.flipnote.user.model.UserRegisterResponse; import project.flipnote.user.service.UserService; @@ -25,4 +28,10 @@ public ResponseEntity register(@Valid @RequestBody UserReg UserRegisterResponse res = userService.register(req); return ResponseEntity.status(HttpStatus.CREATED).body(res); } + + @DeleteMapping + public ResponseEntity unregister(@AuthenticationPrincipal UserAuth userAuth) { + userService.unregister(userAuth.userId()); + return ResponseEntity.noContent().build(); + } } diff --git a/src/main/java/project/flipnote/user/entity/User.java b/src/main/java/project/flipnote/user/entity/User.java index 24e71942..0256974c 100644 --- a/src/main/java/project/flipnote/user/entity/User.java +++ b/src/main/java/project/flipnote/user/entity/User.java @@ -76,4 +76,9 @@ public User( this.role = UserRole.USER; } + @Override + public void softDelete() { + super.softDelete(); + this.status = UserStatus.INACTIVE; + } } diff --git a/src/main/java/project/flipnote/user/repository/UserRepository.java b/src/main/java/project/flipnote/user/repository/UserRepository.java index 05761956..007c48cb 100644 --- a/src/main/java/project/flipnote/user/repository/UserRepository.java +++ b/src/main/java/project/flipnote/user/repository/UserRepository.java @@ -14,4 +14,6 @@ public interface UserRepository extends JpaRepository { boolean existsByPhone(String phone); Optional findByEmailAndStatus(String email, UserStatus status); + + Optional findByIdAndStatus(Long userId, UserStatus status); } diff --git a/src/main/java/project/flipnote/user/service/UserService.java b/src/main/java/project/flipnote/user/service/UserService.java index 05c00e0c..85484f8a 100644 --- a/src/main/java/project/flipnote/user/service/UserService.java +++ b/src/main/java/project/flipnote/user/service/UserService.java @@ -10,6 +10,7 @@ import project.flipnote.auth.service.AuthService; import project.flipnote.common.exception.BizException; import project.flipnote.user.entity.User; +import project.flipnote.user.entity.UserStatus; import project.flipnote.user.exception.UserErrorCode; import project.flipnote.user.model.UserRegisterRequest; import project.flipnote.user.model.UserRegisterResponse; @@ -50,6 +51,18 @@ public UserRegisterResponse register(UserRegisterRequest req) { return UserRegisterResponse.from(savedUser.getId()); } + @Transactional + public void unregister(Long userId) { + User user = findActiveUserById(userId); + + user.softDelete(); + } + + private User findActiveUserById(Long userId) { + return userRepository.findByIdAndStatus(userId, UserStatus.ACTIVE) + .orElseThrow(() -> new BizException(UserErrorCode.USER_NOT_FOUND)); + } + private void validateEmailDuplicate(String email) { if (userRepository.existsByEmail(email)) { throw new BizException(UserErrorCode.DUPLICATE_EMAIL); diff --git a/src/test/java/project/flipnote/user/service/UserServiceTest.java b/src/test/java/project/flipnote/user/service/UserServiceTest.java index 67b8617a..6b5cb480 100644 --- a/src/test/java/project/flipnote/user/service/UserServiceTest.java +++ b/src/test/java/project/flipnote/user/service/UserServiceTest.java @@ -4,6 +4,8 @@ import static org.junit.jupiter.api.Assertions.*; import static org.mockito.BDDMockito.*; +import java.util.Optional; + import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; @@ -18,7 +20,9 @@ import project.flipnote.auth.exception.AuthErrorCode; import project.flipnote.auth.service.AuthService; import project.flipnote.common.exception.BizException; +import project.flipnote.fixture.UserFixture; import project.flipnote.user.entity.User; +import project.flipnote.user.entity.UserStatus; import project.flipnote.user.exception.UserErrorCode; import project.flipnote.user.model.UserRegisterRequest; import project.flipnote.user.model.UserRegisterResponse; @@ -40,23 +44,6 @@ class UserServiceTest { @Mock private PasswordEncoder passwordEncoder; - private User user; - - @BeforeEach - void init() { - user = User.builder() - .email("test@test.com") - .password("testPass") - .name("테스트") - .nickname("테스트") - .smsAgree(false) - .phone("010-1234-5678") - .profileImageUrl(null) - .build(); - - ReflectionTestUtils.setField(user, "id", 1L); - } - @DisplayName("회원가입 테스트") @Nested class Register { @@ -64,6 +51,7 @@ class Register { @DisplayName("성공") @Test void success() { + User user = UserFixture.createActiveUser(); UserRegisterRequest req = new UserRegisterRequest( "test@test.com", "testPass", "테스트", "테스트", false, "010-1234-5678", "" ); @@ -84,6 +72,7 @@ void success() { @DisplayName("휴대전화 번호가 null일 때 성공") @Test void success_ifPhoneIsNull() { + User user = UserFixture.createActiveUser(); UserRegisterRequest req = new UserRegisterRequest( "test@test.com", "testPass", "테스트", "테스트", false, null, null ); @@ -151,4 +140,32 @@ void fail_unverifiedEmail() { } } + @DisplayName("회원 탈퇴 테스트") + @Nested + class Unregister { + + @DisplayName("성공") + @Test + void success() { + User user = spy(UserFixture.createActiveUser()); + + given(userRepository.findByIdAndStatus(anyLong(), any(UserStatus.class))).willReturn(Optional.of(user)); + + userService.unregister(user.getId()); + + assertThat(user.getStatus()).isEqualTo(UserStatus.INACTIVE); + assertThat(user.getDeletedAt()).isNotNull(); + + verify(user, times(1)).softDelete(); + } + + @DisplayName("회원 id가 존재하지 않는 경우 예외 발생") + @Test + void fail_userNotFound() { + given(userRepository.findByIdAndStatus(anyLong(), any(UserStatus.class))).willReturn(Optional.empty()); + + BizException exception = assertThrows(BizException.class, () -> userService.unregister(1L)); + assertThat(exception.getErrorCode()).isEqualTo(UserErrorCode.USER_NOT_FOUND); + } + } }