Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -30,5 +30,7 @@ private UrlConstants() {}
public static final String[] ORIGIN_EXTRACT_PATHS = {"/api/auth/login/kakao"};

// 인증이 필요없는 API 경로
public static final String[] PERMIT_ALL_API_PATHS = {"/api/auth/**", "/api/trips/categories"};
public static final String[] PERMIT_ALL_API_PATHS = {
"/api/auth/**", "/api/trips/categories", "/api/members/me/restore/**"
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,13 @@ public void hardDeleteMemberCascade(Long memberId) {
imageService.publishCleanupBatchEvent(imageUrls);
}

@Transactional
public void restoreMember(Long memberId) {
Member member = memberQueryService.getDeletedMember(memberId);

memberCommandService.restoreMember(member);
}

private List<String> collectImageUrlsForMember(Member member) {
List<String> imageUrls = new ArrayList<>();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,10 @@ public void deleteMember(Member member) {
member.updateDeletedAt();
}

public void restoreMember(Member member) {
member.restoreDeletedAt();
}

public long hardDeleteMembers() {
return memberCommandRepository.deleteAllByDeletedAtIsNotNull();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,17 @@ public Member getValidMember(Long memberId) {
.orElseThrow(() -> new CustomException(MemberErrorCode.MEMBER_NOT_FOUND));
}

public Member getDeletedMember(Long memberId) {
Member member =
memberRepository
.findById(memberId)
.orElseThrow(() -> new CustomException(MemberErrorCode.MEMBER_NOT_FOUND));

MemberPolicy.validateDeleted(member);

return member;
}

public String getRoleByMemberId(String memberId) {
MemberRole memberRole =
memberQueryRepository
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ public enum MemberErrorCode implements ErrorCode {
INVALID_MEMBER_CATEGORY(HttpStatus.BAD_REQUEST, "유효하지 않은 멤버 카테고리입니다."),
MEMBER_NICKNAME_DUPLICATED(HttpStatus.BAD_REQUEST, "이미 사용 중인 닉네임입니다."),
MEMBER_ALREADY_DELETED(HttpStatus.BAD_REQUEST, "해당 멤버는 이미 삭제되었습니다."),
MEMBER_NOT_DELETED(HttpStatus.BAD_REQUEST, "해당 멤버는 삭제되지 않았습니다."),

// 404
MEMBER_NOT_FOUND(HttpStatus.NOT_FOUND, "멤버를 찾을 수 없습니다."),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,10 @@ public void updateDeletedAt() {
this.deletedAt = LocalDateTime.now();
}

public void restoreDeletedAt() {
this.deletedAt = null;
}

public MemberCategory getCategory() {
return category;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,10 @@ public static void validateNotDeleted(Member member) {
throw new CustomException(MemberErrorCode.MEMBER_ALREADY_DELETED);
}
}

public static void validateDeleted(Member member) {
if (member.getDeletedAt() == null) {
throw new CustomException(MemberErrorCode.MEMBER_NOT_DELETED);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotNull;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
Expand Down Expand Up @@ -144,4 +145,13 @@ public ResponseEntity<StandardResponse> deleteMemberHardDelete(

return ResponseEntity.ok().body(StandardResponse.success(HttpStatus.OK.value(), null));
}

@Operation(summary = "멤버 복구", description = "삭제된 멤버를 복구합니다.")
@PatchMapping("/me/restore/{memberId}")
public ResponseEntity<StandardResponse> restoreMember(
@PathVariable @NotNull(message = "멤버 ID는 필수 요청 파라미터입니다.") Long memberId) {
memberFacade.restoreMember(memberId);

return ResponseEntity.ok().body(StandardResponse.success(HttpStatus.OK.value(), null));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -248,6 +248,24 @@ void shouldDeleteMember() {
}
}

@Nested
@DisplayName("restoreMember 메서드는")
class RestoreMember {

@Test
@DisplayName("삭제된 멤버가 복구될 때 deletedAt 필드를 null로 업데이트한다.")
void shouldRestoreDeletedAtWhenDeletedMemberIsRestored() {
// given
member.updateDeletedAt();

// when
memberCommandService.restoreMember(member);

// then
assertThat(member.getDeletedAt()).isNull();
}
}

@Nested
@DisplayName("hardDeleteMembers 메서드는")
class HardDeleteMembers {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,52 @@ void shouldReturnMemberWhenMemberIdIsValid() {
}
}

@Nested
@DisplayName("getDeletedMember 메서드는")
class GetDeletedMember {

@Test
@DisplayName("멤버가 존재하지 않으면 예외가 발생한다.")
void shouldThrowExceptionWhenMemberDoesNotExist() {
// given
Long memberId = -1L;
given(memberRepository.findById(memberId)).willReturn(Optional.empty());

// when & then
assertThatThrownBy(() -> memberQueryService.getDeletedMember(memberId))
.isInstanceOf(CustomException.class)
.hasMessage(MemberErrorCode.MEMBER_NOT_FOUND.getMessage());
}

@Test
@DisplayName("멤버가 삭제되지 않았다면 예외가 발생한다.")
void shouldThrowExceptionWhenMemberIsNotDeleted() {
// given
Long memberId = member.getId();
given(memberRepository.findById(memberId)).willReturn(Optional.of(member));

// when & then
assertThatThrownBy(() -> memberQueryService.getDeletedMember(memberId))
.isInstanceOf(CustomException.class)
.hasMessage(MemberErrorCode.MEMBER_NOT_DELETED.getMessage());
}

@Test
@DisplayName("멤버가 이미 삭제되었다면 삭제된 멤버를 반환한다.")
void shouldReturnMemberWhenMemberAlreadyDeleted() {
// given
Long memberId = member.getId();
member.updateDeletedAt();
given(memberRepository.findById(memberId)).willReturn(Optional.of(member));

// when
Member result = memberQueryService.getDeletedMember(memberId);

// then
assertThat(result).isEqualTo(member);
}
}

@Nested
@DisplayName("getRoleByMemberId 메서드는")
class GetRoleByMemberId {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
import com.ject.studytrip.auth.domain.error.AuthErrorCode;
import com.ject.studytrip.auth.fixture.TokenFixture;
import com.ject.studytrip.auth.helper.TokenTestHelper;
import com.ject.studytrip.global.exception.error.CommonErrorCode;
import com.ject.studytrip.image.domain.error.ImageErrorCode;
import com.ject.studytrip.image.infra.s3.provider.S3ImageStorageProvider;
import com.ject.studytrip.member.domain.error.MemberErrorCode;
Expand Down Expand Up @@ -524,4 +525,99 @@ void shouldHardDeleteMemberAndAllRelatedDataWhenMemberIdIsValid() throws Excepti
.andExpect(jsonPath("$.status").value(HttpStatus.OK.value()));
}
}

@Nested
@DisplayName("멤버 복구 API")
class RestoreMember {
private ResultActions getResultActions(Object memberId) throws Exception {
return mockMvc.perform(
patch(BASE_MEMBER_URL + "/me/restore/{memberId}", memberId)
.contentType(MediaType.APPLICATION_JSON));
}

@Test
@DisplayName("PathVariable 멤버 ID 타입이 올바르지 않으면 400 Bad Request를 반환한다.")
void shouldReturnBadRequestWhenMemberIdTypeMismatch() throws Exception {
// given
String memberId = "abc";

// when
ResultActions resultActions = getResultActions(memberId);

// then
resultActions
.andExpect(status().isBadRequest())
.andExpect(jsonPath("$.success").value(false))
.andExpect(
jsonPath("$.status")
.value(
CommonErrorCode.METHOD_ARGUMENT_TYPE_MISMATCH
.getStatus()
.value()))
.andExpect(
jsonPath("$.data.message")
.value(
CommonErrorCode.METHOD_ARGUMENT_TYPE_MISMATCH
.getMessage()));
}

@Test
@DisplayName("멤버가 존재하지 않으면 404 Not Found를 반환한다.")
void shouldReturnNotFoundWhenMemberDoesNotExist() throws Exception {
// given
Long memberId = -1L;

// when
ResultActions resultActions = getResultActions(memberId);

// then
resultActions
.andExpect(status().isNotFound())
.andExpect(jsonPath("$.success").value(false))
.andExpect(
jsonPath("$.status")
.value(MemberErrorCode.MEMBER_NOT_FOUND.getStatus().value()))
.andExpect(
jsonPath("$.data.message")
.value(MemberErrorCode.MEMBER_NOT_FOUND.getMessage()));
}

@Test
@DisplayName("멤버가 삭제되지 않았다면 400 Bad Request를 반환한다.")
void shouldReturnBadRequestWhenMemberIsNotDeleted() throws Exception {
// given
Long memberId = member.getId();

// when
ResultActions resultActions = getResultActions(memberId);

// then
resultActions
.andExpect(status().isBadRequest())
.andExpect(jsonPath("$.success").value(false))
.andExpect(
jsonPath("$.status")
.value(MemberErrorCode.MEMBER_NOT_DELETED.getStatus().value()))
.andExpect(
jsonPath("$.data.message")
.value(MemberErrorCode.MEMBER_NOT_DELETED.getMessage()));
}

@Test
@DisplayName("유효한 멤버 ID가 들어오면 삭제된 멤버를 복구한다.")
void shouldRestoreMemberWhenMemberIdIsValid() throws Exception {
// given
Long memberId = member.getId();
member.updateDeletedAt();

// when
ResultActions resultActions = getResultActions(memberId);

// then
resultActions
.andExpect(status().isOk())
.andExpect(jsonPath("$.success").value(true))
.andExpect(jsonPath("$.status").value(HttpStatus.OK.value()));
}
}
}