diff --git a/src/main/java/com/dokdok/topic/api/PreOpinionApi.java b/src/main/java/com/dokdok/topic/api/PreOpinionApi.java index a02dbba..99d0cc7 100644 --- a/src/main/java/com/dokdok/topic/api/PreOpinionApi.java +++ b/src/main/java/com/dokdok/topic/api/PreOpinionApi.java @@ -12,6 +12,7 @@ import io.swagger.v3.oas.annotations.tags.Tag; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; @@ -27,7 +28,7 @@ public interface PreOpinionApi { - 권한: 약속의 멤버 - 제약: 본인이 사전 의견을 작성한 경우에만 조회 가능 - 응답: 확정된 주제 목록, 멤버별 사전 의견 (책 평가, 주제 답변) - + **응답 구조** - topics: 확정된 주제 목록 (confirmOrder 순) - members: 멤버별 사전 의견 (프로필, 책 평가, 주제별 답변) @@ -126,4 +127,46 @@ ResponseEntity> findAnswers( @PathVariable("gatheringId") Long gatheringId, @PathVariable("meetingId") Long meetingId ); + + @Operation( + summary = "내 사전의견 삭제 (developer: 경서영)", + description = """ + 현재 로그인 사용자의 약속에 대한 사전의견을 삭제합니다. + - 권한: 약속의 멤버 + """, + parameters = { + @Parameter(name = "gatheringId", description = "모임 식별자", in = ParameterIn.PATH, required = true), + @Parameter(name = "meetingId", description = "약속 식별자", in = ParameterIn.PATH, required = true) + } + ) + @ApiResponses({ + @io.swagger.v3.oas.annotations.responses.ApiResponse( + responseCode = "200", + description = "내 사전의견 삭제 성공", + content = @Content(mediaType = MediaType.APPLICATION_JSON_VALUE, + examples = @ExampleObject(value = """ + {"code":"DELETED","message":"내 사전의견이 삭제되었습니다.","data":null} + """)) + ), + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "403", description = "모임 멤버가 아님", + content = @Content(mediaType = MediaType.APPLICATION_JSON_VALUE, + examples = @ExampleObject(value = """ + {"code": "G002", "message": "모임의 멤버가 아닙니다.", "data": null} + """))), + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "404", description = "답변을 찾을 수 없음", + content = @Content(mediaType = MediaType.APPLICATION_JSON_VALUE, + examples = @ExampleObject(value = """ + {"code": "E103", "message": "답변을 찾을 수 없습니다.", "data": null} + """))), + @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "500", description = "서버 오류", + content = @Content(mediaType = MediaType.APPLICATION_JSON_VALUE, + examples = @ExampleObject(value = """ + {"code": "E000", "message": "서버 에러가 발생했습니다. 담당자에게 문의 바랍니다.", "data": null} + """))) + }) + @DeleteMapping(value = "/me", produces = MediaType.APPLICATION_JSON_VALUE) + ResponseEntity> deleteMyAnswer( + @PathVariable("gatheringId") Long gatheringId, + @PathVariable("meetingId") Long meetingId + ); } diff --git a/src/main/java/com/dokdok/topic/api/TopicAnswerApi.java b/src/main/java/com/dokdok/topic/api/TopicAnswerApi.java index fe6bebc..51193ac 100644 --- a/src/main/java/com/dokdok/topic/api/TopicAnswerApi.java +++ b/src/main/java/com/dokdok/topic/api/TopicAnswerApi.java @@ -226,43 +226,4 @@ ResponseEntity> submitMyAnswer( @PathVariable("topicId") Long topicId ); - @Operation( - summary = "내 토픽 답변 삭제 (developer: 경서영)", - description = """ - 현재 로그인 사용자의 토픽 답변을 삭제합니다. - - 권한: 모임 구성원 - """, - parameters = { - @Parameter(name = "gatheringId", description = "모임 식별자", in = ParameterIn.PATH, required = true), - @Parameter(name = "meetingId", description = "약속 식별자", in = ParameterIn.PATH, required = true), - @Parameter(name = "topicId", description = "토픽 식별자", in = ParameterIn.PATH, required = true) - } - ) - @ApiResponses({ - @io.swagger.v3.oas.annotations.responses.ApiResponse( - responseCode = "200", - description = "내 토픽 답변 삭제 성공" - ), - @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "403", description = "모임 멤버가 아님", - content = @Content(mediaType = MediaType.APPLICATION_JSON_VALUE, - examples = @ExampleObject(value = """ - {"code": "G002", "message": "모임의 멤버가 아닙니다.", "data": null} - """))), - @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "404", description = "답변을 찾을 수 없음", - content = @Content(mediaType = MediaType.APPLICATION_JSON_VALUE, - examples = @ExampleObject(value = """ - {"code": "E103", "message": "답변을 찾을 수 없습니다.", "data": null} - """))), - @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "500", description = "서버 오류", - content = @Content(mediaType = MediaType.APPLICATION_JSON_VALUE, - examples = @ExampleObject(value = """ - {"code": "E000", "message": "서버 에러가 발생했습니다. 담당자에게 문의 바랍니다.", "data": null} - """))) - }) - @DeleteMapping(value = "/me", produces = MediaType.APPLICATION_JSON_VALUE) - ResponseEntity> deleteMyAnswer( - @PathVariable("gatheringId") Long gatheringId, - @PathVariable("meetingId") Long meetingId, - @PathVariable("topicId") Long topicId - ); } diff --git a/src/main/java/com/dokdok/topic/controller/PreOpinionController.java b/src/main/java/com/dokdok/topic/controller/PreOpinionController.java index 22c5553..f51f0d5 100644 --- a/src/main/java/com/dokdok/topic/controller/PreOpinionController.java +++ b/src/main/java/com/dokdok/topic/controller/PreOpinionController.java @@ -7,10 +7,7 @@ import lombok.RequiredArgsConstructor; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.bind.annotation.*; @RestController @RequiredArgsConstructor @@ -28,4 +25,16 @@ public ResponseEntity> findAnswers( PreOpinionResponse response = preOpinionService.findPreOpinions(gatheringId, meetingId); return ApiResponse.success(response, "약속의 사전 의견 목록 조회를 성공했습니다."); } + + @Override + @DeleteMapping(value = "/me", produces = MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity> deleteMyAnswer( + @PathVariable("gatheringId") Long gatheringId, + @PathVariable("meetingId") Long meetingId + ) { + + preOpinionService.deleteMyAnswer(gatheringId, meetingId); + + return ApiResponse.deleted("내 사전의견이 삭제되었습니다."); + } } diff --git a/src/main/java/com/dokdok/topic/controller/TopicAnswerController.java b/src/main/java/com/dokdok/topic/controller/TopicAnswerController.java index a9cccf2..6f5e482 100644 --- a/src/main/java/com/dokdok/topic/controller/TopicAnswerController.java +++ b/src/main/java/com/dokdok/topic/controller/TopicAnswerController.java @@ -71,16 +71,4 @@ public ResponseEntity> submitMyAnswer( return ApiResponse.success(response, "답변이 제출되었습니다."); } - - @Override - public ResponseEntity> deleteMyAnswer( - @PathVariable("gatheringId") Long gatheringId, - @PathVariable("meetingId") Long meetingId, - @PathVariable("topicId") Long topicId - ) { - - topicAnswerService.deleteMyAnswer(gatheringId, meetingId, topicId); - - return ApiResponse.deleted("내 답변이 삭제되었습니다."); - } } \ No newline at end of file diff --git a/src/main/java/com/dokdok/topic/dto/response/PreOpinionResponse.java b/src/main/java/com/dokdok/topic/dto/response/PreOpinionResponse.java index 3ad4e28..c6a0f8f 100644 --- a/src/main/java/com/dokdok/topic/dto/response/PreOpinionResponse.java +++ b/src/main/java/com/dokdok/topic/dto/response/PreOpinionResponse.java @@ -69,8 +69,8 @@ public record MemberPreOpinion( @Schema(description = "멤버 정보") public record MemberInfo( - @Schema(description = "멤버 ID", example = "1") - Long memberId, + @Schema(description = "사용자 ID", example = "1") + Long userId, @Schema(description = "닉네임", example = "독서왕") String nickname, @@ -82,12 +82,12 @@ public record MemberInfo( String role ) { public static MemberInfo of( - Long memberId, + Long userId, String nickname, String profileImage, String role ) { - return new MemberInfo(memberId, nickname, profileImage, role); + return new MemberInfo(userId, nickname, profileImage, role); } } diff --git a/src/main/java/com/dokdok/topic/repository/TopicAnswerRepository.java b/src/main/java/com/dokdok/topic/repository/TopicAnswerRepository.java index 44880dc..2c096cc 100644 --- a/src/main/java/com/dokdok/topic/repository/TopicAnswerRepository.java +++ b/src/main/java/com/dokdok/topic/repository/TopicAnswerRepository.java @@ -92,4 +92,16 @@ WHERE ta.topic.id IN ( """ ) List findByMeetingId(Long meetingId); + + @Query(""" + SELECT ta + FROM TopicAnswer ta + JOIN FETCH ta.topic t + WHERE ta.user.id = :userId + AND t.meeting.id = :meetingId + """) + List findByTopicAnswers( + @Param("meetingId") Long meetingId, + @Param("userId") Long userId + ); } diff --git a/src/main/java/com/dokdok/topic/service/PreOpinionService.java b/src/main/java/com/dokdok/topic/service/PreOpinionService.java index 55c8f13..8ecd406 100644 --- a/src/main/java/com/dokdok/topic/service/PreOpinionService.java +++ b/src/main/java/com/dokdok/topic/service/PreOpinionService.java @@ -58,6 +58,22 @@ public PreOpinionResponse findPreOpinions(Long gatheringId, Long meetingId) { return new PreOpinionResponse(topicInfos, preOpinionData); } + @Transactional + public void deleteMyAnswer( + Long gatheringId, + Long meetingId + ) { + Long userId = SecurityUtil.getCurrentUserId(); + + gatheringValidator.validateGathering(gatheringId); + meetingValidator.validateMeetingInGathering(meetingId, gatheringId); + meetingValidator.validateMeetingMember(meetingId, userId); + + List topicAnswers = topicValidator.getTopicAnswers(meetingId, userId); + + topicAnswers.forEach(TopicAnswer::softDelete); + } + private void validateAccess(Long gatheringId, Long meetingId, Long userId) { gatheringValidator.validateGathering(gatheringId); meetingValidator.validateMeetingInGathering(meetingId, gatheringId); @@ -157,7 +173,7 @@ private PreOpinionResponse.MemberPreOpinion toMemberPreOpinion(MeetingMember mm, String role = resolveRole(mm, maps.gatheringRoleByUserId()); PreOpinionResponse.MemberInfo memberInfo - = PreOpinionResponse.MemberInfo.of(memberId, user.getNickname(), presignedUrl, role); + = PreOpinionResponse.MemberInfo.of(user.getId(), user.getNickname(), presignedUrl, role); BookReview review = maps.bookReviewByUserId().get(memberId); BookReviewInfo bookReviewInfo = review != null diff --git a/src/main/java/com/dokdok/topic/service/TopicAnswerService.java b/src/main/java/com/dokdok/topic/service/TopicAnswerService.java index cb68238..d977ba3 100644 --- a/src/main/java/com/dokdok/topic/service/TopicAnswerService.java +++ b/src/main/java/com/dokdok/topic/service/TopicAnswerService.java @@ -18,6 +18,8 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import java.util.List; + @Service @RequiredArgsConstructor public class TopicAnswerService { @@ -120,21 +122,4 @@ public TopicAnswerSubmitResponse submitMyAnswer( return TopicAnswerSubmitResponse.from(answer); } - @Transactional - public void deleteMyAnswer( - Long gatheringId, - Long meetingId, - Long topicId - ) { - Long userId = SecurityUtil.getCurrentUserId(); - - gatheringValidator.validateGathering(gatheringId); - meetingValidator.validateMeetingInGathering(meetingId, gatheringId); - meetingValidator.validateMeetingMember(meetingId, userId); - topicValidator.validateTopicInMeeting(topicId, meetingId); - - TopicAnswer answer = topicValidator.getTopicAnswer(topicId, userId); - - answer.softDelete(); - } } \ No newline at end of file diff --git a/src/main/java/com/dokdok/topic/service/TopicValidator.java b/src/main/java/com/dokdok/topic/service/TopicValidator.java index c1ad3d7..ad96225 100644 --- a/src/main/java/com/dokdok/topic/service/TopicValidator.java +++ b/src/main/java/com/dokdok/topic/service/TopicValidator.java @@ -66,6 +66,22 @@ public TopicAnswer getTopicAnswer(Long topicId, Long userId) { .orElseThrow(() -> new TopicException(TopicErrorCode.TOPIC_ANSWER_NOT_FOUND)); } + public List getTopicAnswers(Long meetingId, Long userId) { + List topicAnswers = topicAnswerRepository.findByTopicAnswers(meetingId, userId); + + if(topicAnswers.isEmpty()) { + throw new TopicException(TopicErrorCode.TOPIC_ANSWER_NOT_FOUND); + } + + boolean allDeleted = topicAnswers.stream().allMatch(TopicAnswer::isDeleted); + + if (allDeleted) { + throw new TopicException(TopicErrorCode.TOPIC_ANSWER_ALREADY_DELETED); + } + + return topicAnswers; + } + /** * 주제에 대한 삭제 권한 검증한다 * 권한 소유 : 모임장, 약속장, 주제 제안자 diff --git a/src/test/java/com/dokdok/topic/service/PreOpinionServiceTest.java b/src/test/java/com/dokdok/topic/service/PreOpinionServiceTest.java index b7a9b70..b5282a4 100644 --- a/src/test/java/com/dokdok/topic/service/PreOpinionServiceTest.java +++ b/src/test/java/com/dokdok/topic/service/PreOpinionServiceTest.java @@ -9,6 +9,7 @@ import com.dokdok.gathering.entity.GatheringRole; import com.dokdok.gathering.repository.GatheringMemberRepository; import com.dokdok.gathering.service.GatheringValidator; +import com.dokdok.global.exception.GlobalException; import com.dokdok.keyword.entity.Keyword; import com.dokdok.meeting.entity.MeetingMember; import com.dokdok.meeting.entity.MeetingMemberRole; @@ -19,6 +20,8 @@ import com.dokdok.topic.entity.Topic; import com.dokdok.topic.entity.TopicAnswer; import com.dokdok.topic.entity.TopicType; +import com.dokdok.topic.exception.TopicErrorCode; +import com.dokdok.topic.exception.TopicException; import com.dokdok.topic.repository.TopicAnswerRepository; import com.dokdok.topic.repository.TopicRepository; import com.dokdok.user.entity.User; @@ -40,12 +43,13 @@ import java.util.List; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyList; import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.BDDMockito.given; -import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.*; @ExtendWith(MockitoExtension.class) class PreOpinionServiceTest { @@ -238,7 +242,7 @@ void findPreOpinions_success() { // user1 answered -> sorted first PreOpinionResponse.MemberPreOpinion member1Opinion = response.members().get(0); - assertThat(member1Opinion.memberInfo().memberId()).isEqualTo(1L); + assertThat(member1Opinion.memberInfo().userId()).isEqualTo(1L); assertThat(member1Opinion.memberInfo().role()).isEqualTo("GATHERING_LEADER"); assertThat(member1Opinion.bookReview()).isNotNull(); assertThat(member1Opinion.topicOpinions()).isNotEmpty(); @@ -246,7 +250,7 @@ void findPreOpinions_success() { // user2 did not answer -> sorted last PreOpinionResponse.MemberPreOpinion member2Opinion = response.members().get(1); - assertThat(member2Opinion.memberInfo().memberId()).isEqualTo(2L); + assertThat(member2Opinion.memberInfo().userId()).isEqualTo(2L); assertThat(member2Opinion.memberInfo().role()).isEqualTo("MEMBER"); assertThat(member2Opinion.bookReview()).isNull(); assertThat(member2Opinion.topicOpinions()).isEmpty(); @@ -296,9 +300,9 @@ void findPreOpinions_membersOrderedByAnswerTime() { // then - ordered: member3 (earliest) -> member1 (second) -> member2 (no answer) assertThat(response.members()).hasSize(3); - assertThat(response.members().get(0).memberInfo().memberId()).isEqualTo(3L); - assertThat(response.members().get(1).memberInfo().memberId()).isEqualTo(1L); - assertThat(response.members().get(2).memberInfo().memberId()).isEqualTo(2L); + assertThat(response.members().get(0).memberInfo().userId()).isEqualTo(3L); + assertThat(response.members().get(1).memberInfo().userId()).isEqualTo(1L); + assertThat(response.members().get(2).memberInfo().userId()).isEqualTo(2L); } @Test @@ -451,4 +455,115 @@ void findPreOpinions_keywordInfoMappedCorrectly() { assertThat(ki2.name()).isEqualTo("감동적인"); assertThat(ki2.type()).isEqualTo(KeywordType.IMPRESSION); } + + + @Test + @DisplayName("내 사전의견 전체 삭제 시 모든 답변에 softDelete가 호출된다") + void deleteMyAnswer_callsSoftDeleteOnAll() { + Topic topic1 = Topic.builder().id(12L).build(); + Topic topic2 = Topic.builder().id(13L).build(); + User user = User.builder().id(1L).build(); + TopicAnswer answer1 = TopicAnswer.builder() + .id(100L).topic(topic1).user(user).content("답변1").isSubmitted(false).build(); + TopicAnswer answer2 = TopicAnswer.builder() + .id(101L).topic(topic2).user(user).content("답변2").isSubmitted(false).build(); + + doNothing().when(gatheringValidator).validateGathering(1L); + doNothing().when(meetingValidator).validateMeetingInGathering(1L, 1L); + doNothing().when(meetingValidator).validateMeetingMember(1L, 1L); + given(topicValidator.getTopicAnswers(1L, 1L)).willReturn(List.of(answer1, answer2)); + + preOpinionService.deleteMyAnswer(1L, 1L); + + assertThat(answer1.isDeleted()).isTrue(); + assertThat(answer2.isDeleted()).isTrue(); + } + + @Test + @DisplayName("모임 검증 실패 시 답변 삭제가 실패한다") + void deleteMyAnswer_throwsWhenGatheringValidationFails() { + doThrow(new com.dokdok.gathering.exception.GatheringException( + com.dokdok.gathering.exception.GatheringErrorCode.GATHERING_NOT_FOUND)) + .when(gatheringValidator).validateGathering(1L); + + assertThatThrownBy(() -> preOpinionService.deleteMyAnswer(1L, 1L)) + .isInstanceOf(com.dokdok.gathering.exception.GatheringException.class); + + verifyNoInteractions(topicAnswerRepository); + } + + @Test + @DisplayName("미팅 검증 실패 시 답변 삭제가 실패한다") + void deleteMyAnswer_throwsWhenMeetingValidationFails() { + doNothing().when(gatheringValidator).validateGathering(1L); + doThrow(new com.dokdok.meeting.exception.MeetingException( + com.dokdok.meeting.exception.MeetingErrorCode.MEETING_NOT_FOUND)) + .when(meetingValidator).validateMeetingInGathering(1L, 1L); + + assertThatThrownBy(() -> preOpinionService.deleteMyAnswer(1L, 1L)) + .isInstanceOf(com.dokdok.meeting.exception.MeetingException.class); + + verifyNoInteractions(topicAnswerRepository); + } + + @Test + @DisplayName("미팅 멤버 검증 실패 시 답변 삭제가 실패한다") + void deleteMyAnswer_throwsWhenMeetingMemberValidationFails() { + doNothing().when(gatheringValidator).validateGathering(1L); + doNothing().when(meetingValidator).validateMeetingInGathering(1L, 1L); + doThrow(new com.dokdok.meeting.exception.MeetingException( + com.dokdok.meeting.exception.MeetingErrorCode.MEETING_MEMBER_NOT_FOUND)) + .when(meetingValidator).validateMeetingMember(1L, 1L); + + assertThatThrownBy(() -> preOpinionService.deleteMyAnswer(1L, 1L)) + .isInstanceOf(com.dokdok.meeting.exception.MeetingException.class); + + verifyNoInteractions(topicAnswerRepository); + } + + @Test + @DisplayName("답변이 없으면 삭제 시 예외가 발생한다") + void deleteMyAnswer_throwsWhenAnswerNotFound() { + doNothing().when(gatheringValidator).validateGathering(1L); + doNothing().when(meetingValidator).validateMeetingInGathering(1L, 1L); + doNothing().when(meetingValidator).validateMeetingMember(1L, 1L); + given(topicValidator.getTopicAnswers(1L, 1L)) + .willThrow(new TopicException(TopicErrorCode.TOPIC_ANSWER_NOT_FOUND)); + + assertThatThrownBy(() -> preOpinionService.deleteMyAnswer(1L, 1L)) + .isInstanceOf(TopicException.class); + } + + @Test + @DisplayName("이미 삭제된 답변은 다시 삭제할 수 없다") + void deleteMyAnswer_throwsWhenAlreadyDeleted() { + Topic topic = Topic.builder().id(12L).build(); + User user = User.builder().id(1L).build(); + TopicAnswer answer = TopicAnswer.builder() + .id(100L) + .topic(topic) + .user(user) + .content("삭제된 내용") + .isSubmitted(false) + .build(); + answer.softDelete(); + + doNothing().when(gatheringValidator).validateGathering(1L); + doNothing().when(meetingValidator).validateMeetingInGathering(1L, 1L); + doNothing().when(meetingValidator).validateMeetingMember(1L, 1L); + given(topicValidator.getTopicAnswers(1L, 1L)).willReturn(List.of(answer)); + + assertThatThrownBy(() -> preOpinionService.deleteMyAnswer(1L, 1L)) + .isInstanceOf(TopicException.class) + .hasFieldOrPropertyWithValue("errorCode", TopicErrorCode.TOPIC_ANSWER_ALREADY_DELETED); + } + + @Test + @DisplayName("인증 정보가 없으면 삭제 시 예외가 발생한다") + void deleteMyAnswer_throwsWhenUnauthenticated() { + SecurityContextHolder.clearContext(); + + assertThatThrownBy(() -> preOpinionService.deleteMyAnswer(1L, 1L)) + .isInstanceOf(GlobalException.class); + } } \ No newline at end of file diff --git a/src/test/java/com/dokdok/topic/service/TopicAnswerServiceTest.java b/src/test/java/com/dokdok/topic/service/TopicAnswerServiceTest.java index 282e0fe..16e2215 100644 --- a/src/test/java/com/dokdok/topic/service/TopicAnswerServiceTest.java +++ b/src/test/java/com/dokdok/topic/service/TopicAnswerServiceTest.java @@ -12,6 +12,7 @@ import com.dokdok.topic.repository.TopicRepository; import com.dokdok.user.entity.User; import com.dokdok.global.exception.GlobalException; +import java.util.List; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; @@ -247,133 +248,4 @@ void submitMyAnswer_throwsWhenAlreadySubmitted() { )).isInstanceOf(TopicException.class); } - @Test - @DisplayName("내 토픽 답변 삭제 시 softDelete가 호출된다") - void deleteMyAnswer_callsSoftDelete() { - Topic topic = Topic.builder().id(12L).build(); - User user = User.builder().id(1L).build(); - TopicAnswer answer = TopicAnswer.builder() - .id(100L) - .topic(topic) - .user(user) - .content("삭제할 내용") - .isSubmitted(false) - .build(); - - doNothing().when(gatheringValidator).validateGathering(1L); - doNothing().when(meetingValidator).validateMeetingInGathering(1L, 1L); - doNothing().when(meetingValidator).validateMeetingMember(1L, 1L); - doNothing().when(topicValidator).validateTopicInMeeting(12L, 1L); - given(topicValidator.getTopicAnswer(12L, 1L)).willReturn(answer); - - topicAnswerService.deleteMyAnswer(1L, 1L, 12L); - - assertThat(answer.isDeleted()).isTrue(); - } - - @Test - @DisplayName("모임 검증 실패 시 답변 삭제가 실패한다") - void deleteMyAnswer_throwsWhenGatheringValidationFails() { - doThrow(new com.dokdok.gathering.exception.GatheringException( - com.dokdok.gathering.exception.GatheringErrorCode.GATHERING_NOT_FOUND)) - .when(gatheringValidator).validateGathering(1L); - - assertThatThrownBy(() -> topicAnswerService.deleteMyAnswer(1L, 1L, 12L)) - .isInstanceOf(com.dokdok.gathering.exception.GatheringException.class); - - verifyNoInteractions(topicAnswerRepository); - } - - @Test - @DisplayName("미팅 검증 실패 시 답변 삭제가 실패한다") - void deleteMyAnswer_throwsWhenMeetingValidationFails() { - doNothing().when(gatheringValidator).validateGathering(1L); - doThrow(new com.dokdok.meeting.exception.MeetingException( - com.dokdok.meeting.exception.MeetingErrorCode.MEETING_NOT_FOUND)) - .when(meetingValidator).validateMeetingInGathering(1L, 1L); - - assertThatThrownBy(() -> topicAnswerService.deleteMyAnswer(1L, 1L, 12L)) - .isInstanceOf(com.dokdok.meeting.exception.MeetingException.class); - - verifyNoInteractions(topicAnswerRepository); - } - - @Test - @DisplayName("미팅 멤버 검증 실패 시 답변 삭제가 실패한다") - void deleteMyAnswer_throwsWhenMeetingMemberValidationFails() { - doNothing().when(gatheringValidator).validateGathering(1L); - doNothing().when(meetingValidator).validateMeetingInGathering(1L, 1L); - doThrow(new com.dokdok.meeting.exception.MeetingException( - com.dokdok.meeting.exception.MeetingErrorCode.MEETING_MEMBER_NOT_FOUND)) - .when(meetingValidator).validateMeetingMember(1L, 1L); - - assertThatThrownBy(() -> topicAnswerService.deleteMyAnswer(1L, 1L, 12L)) - .isInstanceOf(com.dokdok.meeting.exception.MeetingException.class); - - verifyNoInteractions(topicAnswerRepository); - } - - @Test - @DisplayName("토픽 검증 실패 시 답변 삭제가 실패한다") - void deleteMyAnswer_throwsWhenTopicValidationFails() { - doNothing().when(gatheringValidator).validateGathering(1L); - doNothing().when(meetingValidator).validateMeetingInGathering(1L, 1L); - doNothing().when(meetingValidator).validateMeetingMember(1L, 1L); - doThrow(new TopicException(TopicErrorCode.TOPIC_NOT_FOUND)) - .when(topicValidator).validateTopicInMeeting(12L, 1L); - - assertThatThrownBy(() -> topicAnswerService.deleteMyAnswer(1L, 1L, 12L)) - .isInstanceOf(TopicException.class); - - verifyNoInteractions(topicAnswerRepository); - } - - @Test - @DisplayName("답변이 없으면 삭제 시 예외가 발생한다") - void deleteMyAnswer_throwsWhenAnswerNotFound() { - doNothing().when(gatheringValidator).validateGathering(1L); - doNothing().when(meetingValidator).validateMeetingInGathering(1L, 1L); - doNothing().when(meetingValidator).validateMeetingMember(1L, 1L); - doNothing().when(topicValidator).validateTopicInMeeting(12L, 1L); - given(topicValidator.getTopicAnswer(12L, 1L)) - .willThrow(new TopicException(TopicErrorCode.TOPIC_ANSWER_NOT_FOUND)); - - assertThatThrownBy(() -> topicAnswerService.deleteMyAnswer(1L, 1L, 12L)) - .isInstanceOf(TopicException.class); - } - - @Test - @DisplayName("이미 삭제된 답변은 다시 삭제할 수 없다") - void deleteMyAnswer_throwsWhenAlreadyDeleted() { - Topic topic = Topic.builder().id(12L).build(); - User user = User.builder().id(1L).build(); - TopicAnswer answer = TopicAnswer.builder() - .id(100L) - .topic(topic) - .user(user) - .content("삭제된 내용") - .isSubmitted(false) - .build(); - // 이미 삭제된 상태로 설정 - answer.softDelete(); - - doNothing().when(gatheringValidator).validateGathering(1L); - doNothing().when(meetingValidator).validateMeetingInGathering(1L, 1L); - doNothing().when(meetingValidator).validateMeetingMember(1L, 1L); - doNothing().when(topicValidator).validateTopicInMeeting(12L, 1L); - given(topicValidator.getTopicAnswer(12L, 1L)).willReturn(answer); - - assertThatThrownBy(() -> topicAnswerService.deleteMyAnswer(1L, 1L, 12L)) - .isInstanceOf(TopicException.class) - .hasFieldOrPropertyWithValue("errorCode", TopicErrorCode.TOPIC_ANSWER_ALREADY_DELETED); - } - - @Test - @DisplayName("인증 정보가 없으면 삭제 시 예외가 발생한다") - void deleteMyAnswer_throwsWhenUnauthenticated() { - SecurityContextHolder.clearContext(); - - assertThatThrownBy(() -> topicAnswerService.deleteMyAnswer(1L, 1L, 12L)) - .isInstanceOf(GlobalException.class); - } } \ No newline at end of file