Skip to content

Commit

Permalink
Merge branch 'dev' into fix/184/chat-inquiry-update-date
Browse files Browse the repository at this point in the history
  • Loading branch information
dudxo authored Jan 12, 2025
2 parents f0c2b2b + d979d52 commit 8b8c54f
Show file tree
Hide file tree
Showing 7 changed files with 183 additions and 11 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -28,4 +28,5 @@ public interface AnswerRepository extends JpaRepository<Answer, Long> {
Optional<Answer> findByIdWithMember(Long answerId);

boolean existsByQuestionPostIdAndMember(Long questionPostId, Member member);
boolean existsByQuestionPostId(Long questionPostId);
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import org.springframework.data.domain.Pageable;
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.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PatchMapping;
Expand All @@ -16,6 +17,7 @@
import com.dnd.gongmuin.question_post.dto.request.QuestionPostSearchCondition;
import com.dnd.gongmuin.question_post.dto.request.RegisterQuestionPostRequest;
import com.dnd.gongmuin.question_post.dto.request.UpdateQuestionPostRequest;
import com.dnd.gongmuin.question_post.dto.response.DeleteQuestionPostResponse;
import com.dnd.gongmuin.question_post.dto.response.QuestionPostDetailResponse;
import com.dnd.gongmuin.question_post.dto.response.QuestionPostSimpleResponse;
import com.dnd.gongmuin.question_post.dto.response.RecQuestionPostResponse;
Expand Down Expand Up @@ -94,4 +96,16 @@ public ResponseEntity<UpdateQuestionPostResponse> updateQuestionPosts(
= questionPostService.updateQuestionPost(questionPostId, request);
return ResponseEntity.ok(response);
}

@Operation(summary = "질문글 삭제 API", description = "답변이 없을 시 질문자가 질문글을 삭제할 수 있다.")
@ApiResponse(useReturnTypeSchema = true)
@DeleteMapping("/api/question-posts/{questionPostId}")
public ResponseEntity<DeleteQuestionPostResponse> updateQuestionPosts(
@PathVariable("questionPostId") Long questionPostId,
@AuthenticationPrincipal Member member
) {
DeleteQuestionPostResponse response
= questionPostService.deleteQuestionPost(questionPostId, member);
return ResponseEntity.ok(response);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package com.dnd.gongmuin.question_post.dto.response;

public record DeleteQuestionPostResponse(
int remainingCredit
) {
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ public enum QuestionPostErrorCode implements ErrorCode {

NOT_FOUND_QUESTION_POST("해당 아이디의 질문 포스트가 존재하지 않습니다.", "QP_001"),
NOT_AUTHORIZED("질문글에서 해당 작업 권한이 없습니다.", "QP_002"),
NOT_FOUND_QUESTION_POST_STATUS("해당 질문글의 상태를 찾을 수 없습니다.", "QP_003");
NOT_FOUND_QUESTION_POST_STATUS("해당 질문글의 상태를 찾을 수 없습니다.", "QP_003"),
CAN_NOT_DELETE_QUESTION_POST("답변이 존재하는 질문글은 삭제할 수 없습니다.", "QP_004");

private final String message;
private final String code;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,18 @@

import java.time.LocalDateTime;
import java.util.List;
import java.util.Objects;

import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Slice;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import com.dnd.gongmuin.answer.repository.AnswerRepository;
import com.dnd.gongmuin.common.dto.PageMapper;
import com.dnd.gongmuin.common.dto.PageResponse;
import com.dnd.gongmuin.common.exception.runtime.NotFoundException;
import com.dnd.gongmuin.common.exception.runtime.ValidationException;
import com.dnd.gongmuin.credit_history.domain.CreditType;
import com.dnd.gongmuin.credit_history.service.CreditHistoryService;
import com.dnd.gongmuin.member.domain.JobGroup;
Expand All @@ -26,6 +29,7 @@
import com.dnd.gongmuin.question_post.dto.request.QuestionPostSearchCondition;
import com.dnd.gongmuin.question_post.dto.request.RegisterQuestionPostRequest;
import com.dnd.gongmuin.question_post.dto.request.UpdateQuestionPostRequest;
import com.dnd.gongmuin.question_post.dto.response.DeleteQuestionPostResponse;
import com.dnd.gongmuin.question_post.dto.response.QuestionPostDetailResponse;
import com.dnd.gongmuin.question_post.dto.response.QuestionPostSimpleResponse;
import com.dnd.gongmuin.question_post.dto.response.RecQuestionPostResponse;
Expand All @@ -47,6 +51,7 @@ public class QuestionPostService {
private final QuestionPostImageRepository questionPostImageRepository;
private final MemberRepository memberRepository;
private final CreditHistoryService creditHistoryService;
private final AnswerRepository answerRepository;

private static void updateQuestionPost(UpdateQuestionPostRequest request, QuestionPost questionPost) {
questionPost.updateQuestionPost(
Expand Down Expand Up @@ -122,6 +127,20 @@ public UpdateQuestionPostResponse updateQuestionPost(
return QuestionPostMapper.toUpdateQuestionPostResponse(questionPost);
}

@Transactional
public DeleteQuestionPostResponse deleteQuestionPost(
Long questionPostId, Member member
) {
QuestionPost questionPost = questionPostRepository.findById(questionPostId)
.orElseThrow(() -> new NotFoundException(QuestionPostErrorCode.NOT_FOUND_QUESTION_POST));
validateIfQuestionPostExists(questionPostId);
validateIfQuestioner(member, questionPost);
refundDeletedQuestionPost(questionPost);
questionPostRepository.deleteById(questionPostId);

return new DeleteQuestionPostResponse(questionPost.getMember().getCredit());
}

private void updateQuestionPostImages(QuestionPost questionPost, List<String> imageUrls) {
if (imageUrls != null) { // 수정 사항 존재
deleteImages(questionPost); // 기존 이미지 객체 삭제 (새로 비우기 || 수정할 값 존재)
Expand All @@ -131,6 +150,18 @@ private void updateQuestionPostImages(QuestionPost questionPost, List<String> im
}
}

private void validateIfQuestionPostExists(Long questionPostId) {
if (answerRepository.existsByQuestionPostId(questionPostId)) {
throw new ValidationException(QuestionPostErrorCode.CAN_NOT_DELETE_QUESTION_POST);
}
}

private void validateIfQuestioner(Member member, QuestionPost questionPost){
if (!Objects.equals(member.getId(), questionPost.getMember().getId())){
throw new ValidationException(QuestionPostErrorCode.NOT_AUTHORIZED);
}
}

private void deleteImages(QuestionPost questionPost) {
questionPostImageRepository.deleteByQuestionPost(questionPost);
questionPost.clearPostImages();
Expand All @@ -154,17 +185,29 @@ public void changeQuestionPostStatusAnswerClosed() {
questionPostRepository.updateQuestionPostStatusAnswerClosed(LocalDateTime.now());
}

private void refundQuestionPostCredit() {
private void refundDeletedQuestionPost(QuestionPost questionPost) {
Member member = questionPost.getMember();
int reward = questionPost.getReward();
member.increaseCredit(reward);

saveRefundCreditHistory(member, reward);
}

private void refundClosedQuestionPosts() {
List<RefundQuestionPostDto> refundQuestionPostDtos = questionPostRepository.getRefundQuestionPostDtos();
refundQuestionPostDtos.forEach(refundQuestionPostDto -> {
refundQuestionPostDto.member().increaseCredit(refundQuestionPostDto.reward());
memberRepository.save(refundQuestionPostDto.member());

creditHistoryService.saveCreditHistory(
CreditType.REFUND_QUESTION_POST,
refundQuestionPostDto.reward(),
refundQuestionPostDto.member()
);
saveRefundCreditHistory(refundQuestionPostDto.member(), refundQuestionPostDto.reward());
});
}

private void saveRefundCreditHistory(Member member, int reward) {
memberRepository.save(member);

creditHistoryService.saveCreditHistory(
CreditType.REFUND_QUESTION_POST,
reward,
member
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@
import org.springframework.test.util.ReflectionTestUtils;
import org.springframework.test.web.servlet.result.MockMvcResultHandlers;

import com.dnd.gongmuin.answer.domain.Answer;
import com.dnd.gongmuin.answer.repository.AnswerRepository;
import com.dnd.gongmuin.common.fixture.AnswerFixture;
import com.dnd.gongmuin.common.fixture.InteractionCountFixture;
import com.dnd.gongmuin.common.fixture.InteractionFixture;
import com.dnd.gongmuin.common.fixture.MemberFixture;
Expand All @@ -33,6 +36,7 @@
import com.dnd.gongmuin.question_post.domain.QuestionPost;
import com.dnd.gongmuin.question_post.dto.request.RegisterQuestionPostRequest;
import com.dnd.gongmuin.question_post.dto.request.UpdateQuestionPostRequest;
import com.dnd.gongmuin.question_post.exception.QuestionPostErrorCode;
import com.dnd.gongmuin.question_post.repository.QuestionPostRepository;

@DisplayName("[QuestionPost 통합 테스트]")
Expand All @@ -50,13 +54,17 @@ class QuestionPostControllerTest extends ApiTestSupport {
@Autowired
private CreditHistoryRepository creditHistoryRepository;

@Autowired
private AnswerRepository answerRepository;

@Autowired
private InteractionService interactionService;

@AfterEach
void teardown() {
creditHistoryRepository.deleteAll();
memberRepository.deleteAll();
answerRepository.deleteAll();
questionPostRepository.deleteAll();
interactionRepository.deleteAll();
interactionCountRepository.deleteAll();
Expand Down Expand Up @@ -191,7 +199,7 @@ void searchQuestionPostByCategories() throws Exception {
.andExpect(jsonPath("$.content[0].questionPostId").value(questionPost3.getId()));
}

@DisplayName("[질문글을 필터링 직군이 3개 넘어가면 예외가 발생한다.]")
@DisplayName("[질문글 필터링 직군이 3개 넘어가면 예외가 발생한다.]")
@Test
void searchQuestionPostByCategoriesFails() throws Exception {
QuestionPost questionPost1 = questionPostRepository.save(QuestionPostFixture.questionPost("기계", loginMember));
Expand Down Expand Up @@ -245,7 +253,7 @@ void getRecommendQuestionPosts() throws Exception {
.andExpect(jsonPath("$.content[2].questionPostId").value(questionPost2.getId()));
}

@DisplayName("[질문글 업데이트해 게시물 정보를 수정할 수 있다..]")
@DisplayName("[질문글 게시물 정보를 수정할 수 있다.]")
@Test
void updateQuestionPost() throws Exception {
QuestionPost questionPost = questionPostRepository.save(QuestionPostFixture.questionPost(loginMember));
Expand Down Expand Up @@ -334,6 +342,40 @@ void updateQuestionPost_images_empty() throws Exception {
.andDo(MockMvcResultHandlers.print());
}

@DisplayName("[질문글을 삭제할 수 있다.]")
@Test
void deleteQuestionPost() throws Exception {
QuestionPost questionPost = questionPostRepository.save(QuestionPostFixture.questionPost(loginMember));

int creditBeforeDeletion = loginMember.getCredit();
int creditAfterDeletion = creditBeforeDeletion + questionPost.getReward();

mockMvc.perform(delete("/api/question-posts/{questionPostId}", questionPost.getId())
.cookie(accessToken))
.andExpect(status().isOk())
.andExpect(jsonPath("$.remainingCredit")
.value(creditAfterDeletion))
.andDo(MockMvcResultHandlers.print());
}

@DisplayName("[답변이 있을 경우 질문글을 삭제할 수 없다.]")
@Test
void deleteQuestionPostFails() throws Exception {
QuestionPost questionPost = questionPostRepository.save(
QuestionPostFixture.questionPost(loginMember)
);
answerRepository.save(
AnswerFixture.answer(questionPost.getId(), loginMember)
);

mockMvc.perform(delete("/api/question-posts/{questionPostId}", questionPost.getId())
.cookie(accessToken))
.andExpect(status().isBadRequest())
.andExpect(jsonPath("$.message")
.value(QuestionPostErrorCode.CAN_NOT_DELETE_QUESTION_POST.getMessage()))
.andDo(MockMvcResultHandlers.print());
}

private void interactPost(Long questionPostId, InteractionType type) {
Interaction interaction =
InteractionFixture.interaction(type, 2L, questionPostId);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;

import com.dnd.gongmuin.answer.repository.AnswerRepository;
import com.dnd.gongmuin.common.exception.runtime.ValidationException;
import com.dnd.gongmuin.common.fixture.InteractionCountFixture;
import com.dnd.gongmuin.common.fixture.MemberFixture;
import com.dnd.gongmuin.common.fixture.QuestionPostFixture;
Expand All @@ -31,9 +33,11 @@
import com.dnd.gongmuin.question_post.domain.QuestionPostStatus;
import com.dnd.gongmuin.question_post.dto.request.RegisterQuestionPostRequest;
import com.dnd.gongmuin.question_post.dto.request.UpdateQuestionPostRequest;
import com.dnd.gongmuin.question_post.dto.response.DeleteQuestionPostResponse;
import com.dnd.gongmuin.question_post.dto.response.QuestionPostDetailResponse;
import com.dnd.gongmuin.question_post.dto.response.RegisterQuestionPostResponse;
import com.dnd.gongmuin.question_post.dto.response.UpdateQuestionPostResponse;
import com.dnd.gongmuin.question_post.exception.QuestionPostErrorCode;
import com.dnd.gongmuin.question_post.repository.QuestionPostImageRepository;
import com.dnd.gongmuin.question_post.repository.QuestionPostRepository;

Expand All @@ -58,6 +62,9 @@ class QuestionPostServiceTest {
@Mock
private MemberRepository memberRepository;

@Mock
private AnswerRepository answerRepository;

@Mock
private CreditHistoryService creditHistoryService;

Expand Down Expand Up @@ -256,4 +263,62 @@ void updateQuestionPost() {
.map(QuestionPostImage::getImageUrl).toList())
);
}

@DisplayName("[질문글을 삭제할 수 있다.]")
@Test
void deleteQuestionPost() {
//given
Long questionPostId = 1L;
int previousCredit = member.getCredit();
QuestionPost questionPost = QuestionPostFixture.questionPost(member);

given(questionPostRepository.findById(questionPostId))
.willReturn(Optional.of(questionPost));
given(answerRepository.existsByQuestionPostId(questionPostId)).willReturn(false);

//when
DeleteQuestionPostResponse response
= questionPostService.deleteQuestionPost(questionPostId, member);

//then
assertThat(response.remainingCredit())
.isEqualTo(previousCredit + questionPost.getReward());
}

@DisplayName("[답변이 존재하는 질문글은 삭제할 수 없다.]")
@Test
void deleteQuestionPostFails() {
//given
Long questionPostId = 1L;
QuestionPost questionPost = QuestionPostFixture.questionPost(member);
given(questionPostRepository.findById(questionPostId))
.willReturn(Optional.of(questionPost));
given(answerRepository.existsByQuestionPostId(questionPostId)).willReturn(true);

//when & then
ValidationException exception = assertThrows(ValidationException.class,
() -> questionPostService.deleteQuestionPost(questionPostId, member));

assertThat(exception.getMessage())
.isEqualTo(QuestionPostErrorCode.CAN_NOT_DELETE_QUESTION_POST.getMessage());
}

@DisplayName("[질문글 작성자가 아닌 경우 질문글을 삭제할 수 없다.]")
@Test
void deleteQuestionPostFails2() {
//given
Long questionPostId = 1L;
Member unauthorizedMember = MemberFixture.member(2L);
QuestionPost questionPost = QuestionPostFixture.questionPost(unauthorizedMember);
given(questionPostRepository.findById(questionPostId))
.willReturn(Optional.of(questionPost));
given(answerRepository.existsByQuestionPostId(questionPostId)).willReturn(false);

//when & then
ValidationException exception = assertThrows(ValidationException.class,
() -> questionPostService.deleteQuestionPost(questionPostId, member));

assertThat(exception.getMessage())
.isEqualTo(QuestionPostErrorCode.NOT_AUTHORIZED.getMessage());
}
}

0 comments on commit 8b8c54f

Please sign in to comment.