diff --git a/src/main/java/com/dnd/gongmuin/answer/controller/AnswerController.java b/src/main/java/com/dnd/gongmuin/answer/controller/AnswerController.java index 78da7db5..7c988947 100644 --- a/src/main/java/com/dnd/gongmuin/answer/controller/AnswerController.java +++ b/src/main/java/com/dnd/gongmuin/answer/controller/AnswerController.java @@ -15,15 +15,21 @@ import com.dnd.gongmuin.common.dto.PageResponse; import com.dnd.gongmuin.member.domain.Member; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; +@Tag(name = "답변 API") @RestController @RequiredArgsConstructor @RequestMapping("/api/question-posts") public class AnswerController { private final AnswerService answerService; + @Operation(summary = "답변 등록 API", description = "질문글에 대한 답변을 작성한다.") + @ApiResponse(useReturnTypeSchema = true) @PostMapping("/{questionPostId}/answers") public ResponseEntity registerAnswer( @PathVariable Long questionPostId, @@ -34,6 +40,8 @@ public ResponseEntity registerAnswer( return ResponseEntity.ok(response); } + @Operation(summary = "답변 조회 API", description = "질문글에 속하는 답변을 모두 조회한다.") + @ApiResponse(useReturnTypeSchema = true) @GetMapping("/{questionPostId}/answers") public ResponseEntity> getAnswersByQuestionPostId( @PathVariable Long questionPostId @@ -41,4 +49,15 @@ public ResponseEntity> getAnswersByQuestionPo PageResponse response = answerService.getAnswersByQuestionPostId(questionPostId); return ResponseEntity.ok(response); } + + @Operation(summary = "답변 채택 API", description = "질문자가 답변을 채택한다.") + @ApiResponse(useReturnTypeSchema = true) + @PostMapping("/answers/{answerId}") + public ResponseEntity getAnswersByQuestionPostId( + @PathVariable Long answerId, + @AuthenticationPrincipal Member member + ) { + AnswerDetailResponse response = answerService.chooseAnswer(answerId, member); + return ResponseEntity.ok(response); + } } diff --git a/src/main/java/com/dnd/gongmuin/answer/domain/Answer.java b/src/main/java/com/dnd/gongmuin/answer/domain/Answer.java index 7b5775bc..940ce7da 100644 --- a/src/main/java/com/dnd/gongmuin/answer/domain/Answer.java +++ b/src/main/java/com/dnd/gongmuin/answer/domain/Answer.java @@ -57,4 +57,8 @@ public static Answer of(String content, boolean isQuestioner, Long questionPostI return new Answer(content, isQuestioner, questionPostId, member); } + public void updateIsChosen() { + this.isChosen = true; + } + } diff --git a/src/main/java/com/dnd/gongmuin/answer/dto/RegisterAnswerRequest.java b/src/main/java/com/dnd/gongmuin/answer/dto/RegisterAnswerRequest.java index bd452ad6..1a49af16 100644 --- a/src/main/java/com/dnd/gongmuin/answer/dto/RegisterAnswerRequest.java +++ b/src/main/java/com/dnd/gongmuin/answer/dto/RegisterAnswerRequest.java @@ -7,9 +7,4 @@ public record RegisterAnswerRequest( @NotBlank(message = "답변을 입력해주세요.") String content ) { - public static RegisterAnswerRequest from( - String content - ) { - return new RegisterAnswerRequest(content); - } } \ No newline at end of file diff --git a/src/main/java/com/dnd/gongmuin/answer/exception/AnswerErrorCode.java b/src/main/java/com/dnd/gongmuin/answer/exception/AnswerErrorCode.java new file mode 100644 index 00000000..9afff6fd --- /dev/null +++ b/src/main/java/com/dnd/gongmuin/answer/exception/AnswerErrorCode.java @@ -0,0 +1,17 @@ +package com.dnd.gongmuin.answer.exception; + +import com.dnd.gongmuin.common.exception.ErrorCode; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public enum AnswerErrorCode implements ErrorCode { + + NOT_FOUND_ANSWER("해당 아이디의 답변이 존재하지 않습니다.", "ANS_001"), + ALREADY_CHOSEN_ANSWER_EXISTS("채택한 답변이 존재합니다.", "ANS_02"); + + private final String message; + private final String code; +} diff --git a/src/main/java/com/dnd/gongmuin/answer/service/AnswerService.java b/src/main/java/com/dnd/gongmuin/answer/service/AnswerService.java index 816ae8fa..806de6c8 100644 --- a/src/main/java/com/dnd/gongmuin/answer/service/AnswerService.java +++ b/src/main/java/com/dnd/gongmuin/answer/service/AnswerService.java @@ -8,10 +8,13 @@ import com.dnd.gongmuin.answer.dto.AnswerDetailResponse; import com.dnd.gongmuin.answer.dto.AnswerMapper; import com.dnd.gongmuin.answer.dto.RegisterAnswerRequest; +import com.dnd.gongmuin.answer.exception.AnswerErrorCode; 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.service.CreditHistoryService; import com.dnd.gongmuin.member.domain.Member; import com.dnd.gongmuin.question_post.domain.QuestionPost; import com.dnd.gongmuin.question_post.exception.QuestionPostErrorCode; @@ -25,6 +28,13 @@ public class AnswerService { private final QuestionPostRepository questionPostRepository; private final AnswerRepository answerRepository; + private final CreditHistoryService creditHistoryService; + + private static void validateIfQuestioner(Member member, QuestionPost questionPost) { + if (!questionPost.isQuestioner(member)) { + throw new ValidationException(QuestionPostErrorCode.NOT_AUTHORIZED); + } + } @Transactional public AnswerDetailResponse registerAnswer( @@ -32,11 +42,8 @@ public AnswerDetailResponse registerAnswer( RegisterAnswerRequest request, Member member ) { - QuestionPost questionPost = questionPostRepository.findById(questionPostId) - .orElseThrow(() -> new NotFoundException(QuestionPostErrorCode.NOT_FOUND_QUESTION_POST)); - boolean isQuestioner - = questionPost.getMember().getId().equals(member.getId()); - Answer answer = AnswerMapper.toAnswer(questionPostId, isQuestioner, request, member); + QuestionPost questionPost = findQuestionPostById(questionPostId); + Answer answer = AnswerMapper.toAnswer(questionPostId, questionPost.isQuestioner(member), request, member); return AnswerMapper.toAnswerDetailResponse(answerRepository.save(answer)); } @@ -49,6 +56,26 @@ public PageResponse getAnswersByQuestionPostId(Long questi return PageMapper.toPageResponse(answerResponsePage); } + @Transactional + public AnswerDetailResponse chooseAnswer( + Long answerId, + Member member + ) { + Answer answer = getAnswerById(answerId); + QuestionPost questionPost = findQuestionPostById(answer.getQuestionPostId()); + validateIfQuestioner(member, questionPost); + chooseAnswer(questionPost, answer); + + return AnswerMapper.toAnswerDetailResponse(answer); + } + + private void chooseAnswer(QuestionPost questionPost, Answer answer) { + questionPost.updateIsChosen(answer); + answer.getMember().increaseCredit(questionPost.getReward()); + questionPost.getMember().decreaseCredit(questionPost.getReward()); + creditHistoryService.saveChosenCreditHistory(questionPost, answer); + } + private void validateIfQuestionPostExists(Long questionPostId) { boolean isExists = questionPostRepository.existsById(questionPostId); if (!isExists) { @@ -56,4 +83,13 @@ private void validateIfQuestionPostExists(Long questionPostId) { } } -} + private Answer getAnswerById(Long answerId) { + return answerRepository.findById(answerId) + .orElseThrow(() -> new NotFoundException(AnswerErrorCode.NOT_FOUND_ANSWER)); + } + + private QuestionPost findQuestionPostById(Long questionPostId) { + return questionPostRepository.findById(questionPostId) + .orElseThrow(() -> new NotFoundException(QuestionPostErrorCode.NOT_FOUND_QUESTION_POST)); + } +} \ No newline at end of file diff --git a/src/main/java/com/dnd/gongmuin/credit/CreditDetail.java b/src/main/java/com/dnd/gongmuin/credit/CreditDetail.java deleted file mode 100644 index a34c24b6..00000000 --- a/src/main/java/com/dnd/gongmuin/credit/CreditDetail.java +++ /dev/null @@ -1,27 +0,0 @@ -package com.dnd.gongmuin.credit; - -import java.util.Arrays; - -import lombok.Getter; -import lombok.RequiredArgsConstructor; - -@Getter -@RequiredArgsConstructor -public enum CreditDetail { - - DEPOSIT("입금"), - WITHDRAWAL("출금"); - - private final String label; - - public static CreditDetail of(String input) { - return Arrays.stream(values()) - .filter(detail -> detail.isEqual(input)) - .findAny() - .orElseThrow(IllegalArgumentException::new); - } - - private boolean isEqual(String input) { - return input.equals(this.label); - } -} diff --git a/src/main/java/com/dnd/gongmuin/credit/Credit.java b/src/main/java/com/dnd/gongmuin/credit_history/CreditHistory.java similarity index 73% rename from src/main/java/com/dnd/gongmuin/credit/Credit.java rename to src/main/java/com/dnd/gongmuin/credit_history/CreditHistory.java index 5269a198..7507d326 100644 --- a/src/main/java/com/dnd/gongmuin/credit/Credit.java +++ b/src/main/java/com/dnd/gongmuin/credit_history/CreditHistory.java @@ -1,4 +1,4 @@ -package com.dnd.gongmuin.credit; +package com.dnd.gongmuin.credit_history; import static jakarta.persistence.FetchType.*; @@ -15,27 +15,25 @@ import jakarta.persistence.JoinColumn; import jakarta.persistence.ManyToOne; import lombok.AccessLevel; -import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; @Entity @Getter @NoArgsConstructor(access = AccessLevel.PROTECTED) -public class Credit extends TimeBaseEntity { +public class CreditHistory extends TimeBaseEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) - @Column(name = "credit_id", nullable = false) + @Column(name = "credit_history_id", nullable = false) private Long id; @Enumerated(EnumType.STRING) @Column(name = "type", nullable = false) private CreditType type; - @Enumerated(EnumType.STRING) @Column(name = "detail", nullable = false) - private CreditDetail detail; + private String detail; @Column(name = "amount", nullable = false) private int amount; @@ -44,11 +42,14 @@ public class Credit extends TimeBaseEntity { @JoinColumn(name = "member_id", nullable = false) // 정합성 중요 private Member member; - @Builder - public Credit(CreditType type, CreditDetail detail, int amount, Member member) { + private CreditHistory(CreditType type, String detail, int amount, Member member) { this.type = type; this.detail = detail; this.amount = amount; this.member = member; } + + public static CreditHistory of(CreditType type, String detail, int amount, Member member) { + return new CreditHistory(type, detail, amount, member); + } } diff --git a/src/main/java/com/dnd/gongmuin/credit/CreditType.java b/src/main/java/com/dnd/gongmuin/credit_history/CreditType.java similarity index 66% rename from src/main/java/com/dnd/gongmuin/credit/CreditType.java rename to src/main/java/com/dnd/gongmuin/credit_history/CreditType.java index 0d548f4b..fc8fc959 100644 --- a/src/main/java/com/dnd/gongmuin/credit/CreditType.java +++ b/src/main/java/com/dnd/gongmuin/credit_history/CreditType.java @@ -1,4 +1,4 @@ -package com.dnd.gongmuin.credit; +package com.dnd.gongmuin.credit_history; import java.util.Arrays; @@ -9,12 +9,13 @@ @RequiredArgsConstructor public enum CreditType { - CHOOSE("채택하기"), - CHOSEN("채택받기"), - CHAT_REQUEST("채팅신청"), - CHAT_ACCEPT("채팅받기"); + CHOOSE("채택하기", "출금"), + CHOSEN("채택받기", "입금"), + CHAT_REQUEST("채팅신청", "출금"), + CHAT_ACCEPT("채팅받기", "입금"); private final String label; + private final String detail; public static CreditType of(String input) { return Arrays.stream(values()) diff --git a/src/main/java/com/dnd/gongmuin/credit_history/dto/CreditHistoryMapper.java b/src/main/java/com/dnd/gongmuin/credit_history/dto/CreditHistoryMapper.java new file mode 100644 index 00000000..46e78927 --- /dev/null +++ b/src/main/java/com/dnd/gongmuin/credit_history/dto/CreditHistoryMapper.java @@ -0,0 +1,15 @@ +package com.dnd.gongmuin.credit_history.dto; + +import com.dnd.gongmuin.credit_history.CreditHistory; +import com.dnd.gongmuin.credit_history.CreditType; +import com.dnd.gongmuin.member.domain.Member; + +import lombok.AccessLevel; +import lombok.NoArgsConstructor; + +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public class CreditHistoryMapper { + public static CreditHistory toCreditHistory(CreditType creditType, int reward, Member member) { + return CreditHistory.of(creditType, creditType.getDetail(), reward, member); + } +} diff --git a/src/main/java/com/dnd/gongmuin/credit_history/repository/CreditHistoryRepository.java b/src/main/java/com/dnd/gongmuin/credit_history/repository/CreditHistoryRepository.java new file mode 100644 index 00000000..cb0f194d --- /dev/null +++ b/src/main/java/com/dnd/gongmuin/credit_history/repository/CreditHistoryRepository.java @@ -0,0 +1,8 @@ +package com.dnd.gongmuin.credit_history.repository; + +import org.springframework.data.jpa.repository.JpaRepository; + +import com.dnd.gongmuin.credit_history.CreditHistory; + +public interface CreditHistoryRepository extends JpaRepository { +} diff --git a/src/main/java/com/dnd/gongmuin/credit_history/service/CreditHistoryService.java b/src/main/java/com/dnd/gongmuin/credit_history/service/CreditHistoryService.java new file mode 100644 index 00000000..e3601ba0 --- /dev/null +++ b/src/main/java/com/dnd/gongmuin/credit_history/service/CreditHistoryService.java @@ -0,0 +1,29 @@ +package com.dnd.gongmuin.credit_history.service; + +import java.util.List; + +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import com.dnd.gongmuin.answer.domain.Answer; +import com.dnd.gongmuin.credit_history.CreditType; +import com.dnd.gongmuin.credit_history.dto.CreditHistoryMapper; +import com.dnd.gongmuin.credit_history.repository.CreditHistoryRepository; +import com.dnd.gongmuin.question_post.domain.QuestionPost; + +import lombok.RequiredArgsConstructor; + +@Service +@RequiredArgsConstructor +public class CreditHistoryService { + + private final CreditHistoryRepository creditHistoryRepository; + + @Transactional + public void saveChosenCreditHistory(QuestionPost questionPost, Answer answer) { + creditHistoryRepository.saveAll(List.of( + CreditHistoryMapper.toCreditHistory(CreditType.CHOSEN, questionPost.getReward(), answer.getMember()), + CreditHistoryMapper.toCreditHistory(CreditType.CHOOSE, questionPost.getReward(), questionPost.getMember()) + )); + } +} diff --git a/src/main/java/com/dnd/gongmuin/member/domain/Member.java b/src/main/java/com/dnd/gongmuin/member/domain/Member.java index 3ae5700d..2b72bf48 100644 --- a/src/main/java/com/dnd/gongmuin/member/domain/Member.java +++ b/src/main/java/com/dnd/gongmuin/member/domain/Member.java @@ -5,6 +5,8 @@ import static lombok.AccessLevel.*; import com.dnd.gongmuin.common.entity.TimeBaseEntity; +import com.dnd.gongmuin.common.exception.runtime.ValidationException; +import com.dnd.gongmuin.member.exception.MemberErrorCode; import jakarta.persistence.Column; import jakarta.persistence.Entity; @@ -93,4 +95,15 @@ public void updateAdditionalInfo(String nickname, String officialEmail, this.jobCategory = jobCategory; } + public void decreaseCredit(int credit) { + if (this.credit < credit) { + throw new ValidationException(MemberErrorCode.NOT_ENOUGH_CREDIT); + } + this.credit -= credit; + } + + public void increaseCredit(int credit) { + this.credit += credit; + } + } diff --git a/src/main/java/com/dnd/gongmuin/member/exception/MemberErrorCode.java b/src/main/java/com/dnd/gongmuin/member/exception/MemberErrorCode.java index 433c7486..9d86cd4f 100644 --- a/src/main/java/com/dnd/gongmuin/member/exception/MemberErrorCode.java +++ b/src/main/java/com/dnd/gongmuin/member/exception/MemberErrorCode.java @@ -11,7 +11,8 @@ public enum MemberErrorCode implements ErrorCode { NOT_FOUND_MEMBER("특정 회원을 찾을 수 없습니다.", "MEMBER_001"), NOT_FOUND_NEW_MEMBER("신규 회원이 아닙니다.", "MEMBER_002"), - FAIL_LOGOUT("로그아웃을 실패했습니다.", "MEMBER_003"); + FAIL_LOGOUT("로그아웃을 실패했습니다.", "MEMBER_003"), + NOT_ENOUGH_CREDIT("보유한 크레딧이 부족합니다.", "MEMBER_004"); private final String message; private final String code; diff --git a/src/main/java/com/dnd/gongmuin/question_post/controller/QuestionPostController.java b/src/main/java/com/dnd/gongmuin/question_post/controller/QuestionPostController.java index 36c5de44..394c40ee 100644 --- a/src/main/java/com/dnd/gongmuin/question_post/controller/QuestionPostController.java +++ b/src/main/java/com/dnd/gongmuin/question_post/controller/QuestionPostController.java @@ -17,6 +17,7 @@ import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; @Tag(name = "질문글 API") @@ -27,20 +28,20 @@ public class QuestionPostController { private final QuestionPostService questionPostService; - @PostMapping @Operation(summary = "질문글 등록 API", description = "질문글을 등록한다") @ApiResponse(useReturnTypeSchema = true) + @PostMapping public ResponseEntity registerQuestionPost( - @RequestBody RegisterQuestionPostRequest request, + @Valid @RequestBody RegisterQuestionPostRequest request, @AuthenticationPrincipal Member member ) { QuestionPostDetailResponse response = questionPostService.registerQuestionPost(request, member); return ResponseEntity.ok(response); } - @GetMapping("/{questionPostId}") @Operation(summary = "질문글 상세 조회 API", description = "질문글을 아이디로 상세조회한다.") @ApiResponse(useReturnTypeSchema = true) + @GetMapping("/{questionPostId}") public ResponseEntity getQuestionPostById( @PathVariable("questionPostId") Long questionPostId ) { diff --git a/src/main/java/com/dnd/gongmuin/question_post/domain/QuestionPost.java b/src/main/java/com/dnd/gongmuin/question_post/domain/QuestionPost.java index 6b6cb60a..0638ee8a 100644 --- a/src/main/java/com/dnd/gongmuin/question_post/domain/QuestionPost.java +++ b/src/main/java/com/dnd/gongmuin/question_post/domain/QuestionPost.java @@ -5,8 +5,12 @@ import java.util.ArrayList; import java.util.List; +import java.util.Objects; +import com.dnd.gongmuin.answer.domain.Answer; +import com.dnd.gongmuin.answer.exception.AnswerErrorCode; import com.dnd.gongmuin.common.entity.TimeBaseEntity; +import com.dnd.gongmuin.common.exception.runtime.ValidationException; import com.dnd.gongmuin.member.domain.JobGroup; import com.dnd.gongmuin.member.domain.Member; @@ -77,4 +81,15 @@ private void addImages(List images) { image.addQuestionPost(this); }); } + + public boolean isQuestioner(Member member) { + return Objects.equals(this.member.getId(), member.getId()); + } + + public void updateIsChosen(Answer answer) { + if (Boolean.TRUE.equals(this.isChosen)) + throw new ValidationException(AnswerErrorCode.ALREADY_CHOSEN_ANSWER_EXISTS); + this.isChosen = true; + answer.updateIsChosen(); + } } \ No newline at end of file diff --git a/src/main/java/com/dnd/gongmuin/question_post/exception/QuestionPostErrorCode.java b/src/main/java/com/dnd/gongmuin/question_post/exception/QuestionPostErrorCode.java index cfac1d6e..d8b5e61c 100644 --- a/src/main/java/com/dnd/gongmuin/question_post/exception/QuestionPostErrorCode.java +++ b/src/main/java/com/dnd/gongmuin/question_post/exception/QuestionPostErrorCode.java @@ -10,7 +10,7 @@ public enum QuestionPostErrorCode implements ErrorCode { NOT_FOUND_QUESTION_POST("해당 아이디의 질문 포스트가 존재하지 않습니다.", "QP_001"), - NOT_ENOUGH_CREDIT("보유한 크레딧이 부족합니다.", "QP_002"); + NOT_AUTHORIZED("질문글에서 해당 작업 권한이 없습니다.", "QP_002"); private final String message; private final String code; diff --git a/src/main/java/com/dnd/gongmuin/question_post/service/QuestionPostService.java b/src/main/java/com/dnd/gongmuin/question_post/service/QuestionPostService.java index ff341003..7c481d90 100644 --- a/src/main/java/com/dnd/gongmuin/question_post/service/QuestionPostService.java +++ b/src/main/java/com/dnd/gongmuin/question_post/service/QuestionPostService.java @@ -6,6 +6,7 @@ import com.dnd.gongmuin.common.exception.runtime.NotFoundException; import com.dnd.gongmuin.common.exception.runtime.ValidationException; import com.dnd.gongmuin.member.domain.Member; +import com.dnd.gongmuin.member.exception.MemberErrorCode; import com.dnd.gongmuin.question_post.domain.QuestionPost; import com.dnd.gongmuin.question_post.dto.QuestionPostDetailResponse; import com.dnd.gongmuin.question_post.dto.QuestionPostMapper; @@ -13,7 +14,6 @@ import com.dnd.gongmuin.question_post.exception.QuestionPostErrorCode; import com.dnd.gongmuin.question_post.repository.QuestionPostRepository; -import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; @Service @@ -23,9 +23,9 @@ public class QuestionPostService { private final QuestionPostRepository questionPostRepository; @Transactional - public QuestionPostDetailResponse registerQuestionPost(@Valid RegisterQuestionPostRequest request, Member member) { + public QuestionPostDetailResponse registerQuestionPost(RegisterQuestionPostRequest request, Member member) { if (member.getCredit() < request.reward()) { - throw new ValidationException(QuestionPostErrorCode.NOT_ENOUGH_CREDIT); + throw new ValidationException(MemberErrorCode.NOT_ENOUGH_CREDIT); } QuestionPost questionPost = QuestionPostMapper.toQuestionPost(request, member); return QuestionPostMapper.toQuestionPostDetailResponse(questionPostRepository.save(questionPost)); diff --git a/src/test/java/com/dnd/gongmuin/answer/controller/AnswerControllerTest.java b/src/test/java/com/dnd/gongmuin/answer/controller/AnswerControllerTest.java index 7fd48e8b..4409e563 100644 --- a/src/test/java/com/dnd/gongmuin/answer/controller/AnswerControllerTest.java +++ b/src/test/java/com/dnd/gongmuin/answer/controller/AnswerControllerTest.java @@ -19,6 +19,7 @@ import com.dnd.gongmuin.common.fixture.MemberFixture; import com.dnd.gongmuin.common.fixture.QuestionPostFixture; import com.dnd.gongmuin.common.support.ApiTestSupport; +import com.dnd.gongmuin.credit_history.repository.CreditHistoryRepository; import com.dnd.gongmuin.member.domain.Member; import com.dnd.gongmuin.member.repository.MemberRepository; import com.dnd.gongmuin.question_post.domain.QuestionPost; @@ -36,8 +37,12 @@ class AnswerControllerTest extends ApiTestSupport { @Autowired private AnswerRepository answerRepository; + @Autowired + private CreditHistoryRepository creditHistoryRepository; + @AfterEach void teardown() { + creditHistoryRepository.deleteAll(); memberRepository.deleteAll(); questionPostRepository.deleteAll(); answerRepository.deleteAll(); @@ -50,7 +55,7 @@ void registerAnswerByOther() throws Exception { Member anotherMember = memberRepository.save(MemberFixture.member2()); QuestionPost questionPost = questionPostRepository.save(QuestionPostFixture.questionPost(anotherMember)); - RegisterAnswerRequest request = RegisterAnswerRequest.from("본문"); + RegisterAnswerRequest request = new RegisterAnswerRequest("본문"); mockMvc.perform(post("/api/question-posts/{questionPostId}/answers", questionPost.getId()) .content(toJson(request)) .contentType(APPLICATION_JSON) @@ -72,7 +77,7 @@ void registerAnswerByQuestioner() throws Exception { QuestionPost questionPost = questionPostRepository.save(QuestionPostFixture.questionPost(loginMember)); - RegisterAnswerRequest request = RegisterAnswerRequest.from("본문"); + RegisterAnswerRequest request = new RegisterAnswerRequest("본문"); mockMvc.perform(post("/api/question-posts/{questionPostId}/answers", questionPost.getId()) .content(toJson(request)) .contentType(APPLICATION_JSON) @@ -100,7 +105,7 @@ void getAnswersByQuestionPostId() throws Exception { answerRepository.save(AnswerFixture.answer(questionPost.getId(), anotherMember)) )); - RegisterAnswerRequest request = RegisterAnswerRequest.from("본문"); + RegisterAnswerRequest request = new RegisterAnswerRequest("본문"); mockMvc.perform(get("/api/question-posts/{questionPostId}/answers", questionPost.getId()) .content(toJson(request)) .contentType(APPLICATION_JSON) @@ -113,4 +118,25 @@ void getAnswersByQuestionPostId() throws Exception { .andExpect(jsonPath("$.content[1].answerId").value(answers.get(1).getId())) .andExpect(jsonPath("$.content[1].isQuestioner").value(false)); } + + @DisplayName("[질문자는 답변을 채택할 수 있다.]") + @Test + void chooseAnswer() throws Exception { + QuestionPost questionPost + = questionPostRepository.save(QuestionPostFixture.questionPost(loginMember)); + Member answerer = memberRepository.save(MemberFixture.member2()); + Answer answer = answerRepository.save(AnswerFixture.answer(questionPost.getId(), answerer)); + + mockMvc.perform(post("/api/question-posts/answers/{answerId}", answer.getId()) + .header(AUTHORIZATION, accessToken) + ) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.content").value(answer.getContent())) + .andExpect(jsonPath("$.isChosen").value(true)) + .andExpect(jsonPath("$.isQuestioner").value(false)) + .andExpect(jsonPath("$.memberInfo.memberId").value(answerer.getId())) + .andExpect(jsonPath("$.memberInfo.nickname").value(answerer.getNickname())) + .andExpect(jsonPath("$.memberInfo.memberJobGroup").value(answerer.getJobGroup().getLabel()) + ); + } } \ No newline at end of file diff --git a/src/test/java/com/dnd/gongmuin/answer/service/AnswerServiceTest.java b/src/test/java/com/dnd/gongmuin/answer/service/AnswerServiceTest.java index 01f58ccd..17a80223 100644 --- a/src/test/java/com/dnd/gongmuin/answer/service/AnswerServiceTest.java +++ b/src/test/java/com/dnd/gongmuin/answer/service/AnswerServiceTest.java @@ -1,5 +1,7 @@ package com.dnd.gongmuin.answer.service; +import static org.assertj.core.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.*; import static org.mockito.BDDMockito.*; import java.util.List; @@ -15,16 +17,22 @@ import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.SliceImpl; +import org.springframework.test.util.ReflectionTestUtils; import com.dnd.gongmuin.answer.domain.Answer; import com.dnd.gongmuin.answer.dto.AnswerDetailResponse; import com.dnd.gongmuin.answer.dto.RegisterAnswerRequest; import com.dnd.gongmuin.answer.repository.AnswerRepository; import com.dnd.gongmuin.common.dto.PageResponse; +import com.dnd.gongmuin.common.exception.runtime.ValidationException; import com.dnd.gongmuin.common.fixture.AnswerFixture; import com.dnd.gongmuin.common.fixture.MemberFixture; import com.dnd.gongmuin.common.fixture.QuestionPostFixture; +import com.dnd.gongmuin.credit_history.service.CreditHistoryService; +import com.dnd.gongmuin.member.domain.Member; +import com.dnd.gongmuin.member.exception.MemberErrorCode; import com.dnd.gongmuin.question_post.domain.QuestionPost; +import com.dnd.gongmuin.question_post.exception.QuestionPostErrorCode; import com.dnd.gongmuin.question_post.repository.QuestionPostRepository; @DisplayName("[AnswerService 테스트]") @@ -39,6 +47,9 @@ class AnswerServiceTest { @Mock private AnswerRepository answerRepository; + @Mock + private CreditHistoryService creditHistoryService; + @InjectMocks private AnswerService answerService; @@ -49,7 +60,7 @@ void registerAnswer() { Long questionPostId = 1L; Answer answer = AnswerFixture.answer(1L, questionPostId); RegisterAnswerRequest request = - RegisterAnswerRequest.from("답변 내용"); + new RegisterAnswerRequest("답변 내용"); given(questionPostRepository.findById(questionPostId)) .willReturn(Optional.of(QuestionPostFixture.questionPost(questionPostId))); @@ -83,8 +94,74 @@ void getAnswerByQuestionPostId() { questionPostId); //then - Assertions.assertThat(response.content()).hasSize(2); - Assertions.assertThat(response.hasNext()).isFalse(); - Assertions.assertThat(response.content().get(0).answerId()).isEqualTo(answer1.getId()); + assertAll( + () -> assertThat(response.content()).hasSize(2), + () -> assertThat(response.hasNext()).isFalse(), + () -> assertThat(response.content().get(0).answerId()).isEqualTo(answer1.getId()) + ); + } + + @DisplayName("[답변을 채택할 수 있다.]") + @Test + void chooseAnswer() { + //given + Long questionPostId = 1L; + Member member = MemberFixture.member(1L); + QuestionPost questionPost = QuestionPostFixture.questionPost(questionPostId, member); + Answer answer = AnswerFixture.answer(1L, questionPostId); + + given(answerRepository.findById(answer.getId())) + .willReturn(Optional.of(answer)); + given(questionPostRepository.findById(questionPost.getId())) + .willReturn(Optional.of(questionPost)); + + //when + AnswerDetailResponse response = answerService.chooseAnswer(answer.getId(), member); + + //then + Assertions.assertThat(response.isChosen()).isTrue(); + } + + @DisplayName("[크레딧이 부족하면 답변을 채택할 수 없다.]") + @Test + void chooseAnswer_fail() { + //given + Long questionPostId = 1L; + Member member = MemberFixture.member(1L); + QuestionPost questionPost = QuestionPostFixture.questionPost(questionPostId, member); + ReflectionTestUtils.setField(questionPost, "reward", member.getCredit() + 1); + Answer answer = AnswerFixture.answer(1L, questionPostId); + + given(answerRepository.findById(answer.getId())) + .willReturn(Optional.of(answer)); + given(questionPostRepository.findById(questionPost.getId())) + .willReturn(Optional.of(questionPost)); + + //when & then + assertThatThrownBy(() -> answerService.chooseAnswer(answer.getId(), member)) + .isInstanceOf(ValidationException.class) + .hasMessageContaining(MemberErrorCode.NOT_ENOUGH_CREDIT.getMessage()); + + } + + @DisplayName("[질문자가 아니면 채택할 수 없다.]") + @Test + void chooseAnswer_fail2() { + //given + Long questionPostId = 1L; + Member questioner = MemberFixture.member(1L); + Member notQuestioner = MemberFixture.member(2L); + QuestionPost questionPost = QuestionPostFixture.questionPost(questionPostId, questioner); + Answer answer = AnswerFixture.answer(1L, questionPostId); + + given(answerRepository.findById(answer.getId())) + .willReturn(Optional.of(answer)); + given(questionPostRepository.findById(questionPost.getId())) + .willReturn(Optional.of(questionPost)); + + //when & then + assertThatThrownBy(() -> answerService.chooseAnswer(answer.getId(), notQuestioner)) + .isInstanceOf(ValidationException.class) + .hasMessageContaining(QuestionPostErrorCode.NOT_AUTHORIZED.getMessage()); } } \ No newline at end of file diff --git a/src/test/java/com/dnd/gongmuin/common/fixture/QuestionPostFixture.java b/src/test/java/com/dnd/gongmuin/common/fixture/QuestionPostFixture.java index 1cb2898b..ef4fd0ca 100644 --- a/src/test/java/com/dnd/gongmuin/common/fixture/QuestionPostFixture.java +++ b/src/test/java/com/dnd/gongmuin/common/fixture/QuestionPostFixture.java @@ -48,4 +48,22 @@ public static QuestionPost questionPost(Long questionPostId) { return questionPost; } + + public static QuestionPost questionPost(Long questionPostId, Member member) { + QuestionPost questionPost = QuestionPost.of( + "제목", + "내용", + 1000, + JobGroup.of("공업"), + List.of( + QuestionPostImage.from("image1.jpg"), + QuestionPostImage.from("image2.jpg") + ), + member + ); + ReflectionTestUtils.setField(questionPost, "id", questionPostId); + ReflectionTestUtils.setField(questionPost, "createdAt", LocalDateTime.now()); + + return questionPost; + } } diff --git a/src/test/java/com/dnd/gongmuin/credit_history/fixture/CreditHistoryFixture.java b/src/test/java/com/dnd/gongmuin/credit_history/fixture/CreditHistoryFixture.java new file mode 100644 index 00000000..dbae2307 --- /dev/null +++ b/src/test/java/com/dnd/gongmuin/credit_history/fixture/CreditHistoryFixture.java @@ -0,0 +1,21 @@ +package com.dnd.gongmuin.credit_history.fixture; + +import com.dnd.gongmuin.credit_history.CreditHistory; +import com.dnd.gongmuin.credit_history.CreditType; +import com.dnd.gongmuin.member.domain.Member; + +import lombok.AccessLevel; +import lombok.NoArgsConstructor; + +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public class CreditHistoryFixture { + + public static CreditHistory creditHistory(CreditType creditType, int reward, Member member) { + return CreditHistory.of( + creditType, + creditType.getDetail(), + reward, + member + ); + } +} diff --git a/src/test/java/com/dnd/gongmuin/credit_history/service/CreditHistoryServiceTest.java b/src/test/java/com/dnd/gongmuin/credit_history/service/CreditHistoryServiceTest.java new file mode 100644 index 00000000..af74c85f --- /dev/null +++ b/src/test/java/com/dnd/gongmuin/credit_history/service/CreditHistoryServiceTest.java @@ -0,0 +1,48 @@ +package com.dnd.gongmuin.credit_history.service; + +import static org.mockito.BDDMockito.*; + +import java.util.List; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import com.dnd.gongmuin.answer.domain.Answer; +import com.dnd.gongmuin.common.fixture.AnswerFixture; +import com.dnd.gongmuin.common.fixture.MemberFixture; +import com.dnd.gongmuin.common.fixture.QuestionPostFixture; +import com.dnd.gongmuin.credit_history.CreditHistory; +import com.dnd.gongmuin.credit_history.CreditType; +import com.dnd.gongmuin.credit_history.fixture.CreditHistoryFixture; +import com.dnd.gongmuin.credit_history.repository.CreditHistoryRepository; +import com.dnd.gongmuin.question_post.domain.QuestionPost; + +@DisplayName("[AnswerService 테스트]") +@ExtendWith(MockitoExtension.class) +class CreditHistoryServiceTest { + + @Mock + private CreditHistoryRepository creditHistoryRepository; + + @InjectMocks + private CreditHistoryService creditHistoryService; + + @DisplayName("[크레딧 내역을 저장할 수 있다.]") + @Test + void saveChosenCreditHistory() { + QuestionPost questionPost = QuestionPostFixture.questionPost(MemberFixture.member(1L)); + Answer answer = AnswerFixture.answer(questionPost.getId(), MemberFixture.member(2L)); + + List creditHistories = List.of( + CreditHistoryFixture.creditHistory(CreditType.CHOOSE, questionPost.getReward(), questionPost.getMember()), + CreditHistoryFixture.creditHistory(CreditType.CHOSEN, questionPost.getReward(), answer.getMember()) + ); + given(creditHistoryRepository.saveAll(anyList())).willReturn(creditHistories); + + creditHistoryService.saveChosenCreditHistory(questionPost, answer); + } +} \ No newline at end of file diff --git a/src/test/java/com/dnd/gongmuin/question_post/controller/QuestionPostControllerTest.java b/src/test/java/com/dnd/gongmuin/question_post/controller/QuestionPostControllerTest.java index 328fecc5..532d56f6 100644 --- a/src/test/java/com/dnd/gongmuin/question_post/controller/QuestionPostControllerTest.java +++ b/src/test/java/com/dnd/gongmuin/question_post/controller/QuestionPostControllerTest.java @@ -14,9 +14,9 @@ import com.dnd.gongmuin.common.fixture.QuestionPostFixture; import com.dnd.gongmuin.common.support.ApiTestSupport; +import com.dnd.gongmuin.member.exception.MemberErrorCode; import com.dnd.gongmuin.question_post.domain.QuestionPost; import com.dnd.gongmuin.question_post.dto.RegisterQuestionPostRequest; -import com.dnd.gongmuin.question_post.exception.QuestionPostErrorCode; import com.dnd.gongmuin.question_post.repository.QuestionPostRepository; @DisplayName("[QuestionPost 통합 테스트]") @@ -36,9 +36,9 @@ void teardown() { void registerQuestionPost() throws Exception { RegisterQuestionPostRequest request = RegisterQuestionPostRequest.of( "제목", - "내용", + "정정기간에 여석이 있을까요?", List.of("image1.jpg", "image2.jpg"), - 1000, + 2000, "공업" ); @@ -62,9 +62,12 @@ void registerQuestionPost() throws Exception { @DisplayName("[보유 크레딧이 부족하면 질문글을 등록할 수 없다.]") @Test void registerQuestionPostFail() throws Exception { + loginMember.decreaseCredit(5000); + memberRepository.save(loginMember); // 크레딧 + RegisterQuestionPostRequest request = RegisterQuestionPostRequest.of( "제목", - "내용", + "정정기간에 여석이 있을까요?", List.of("image1.jpg", "image2.jpg"), loginMember.getCredit() + 1, "공업" @@ -77,7 +80,7 @@ void registerQuestionPostFail() throws Exception { ) .andExpect(status().isBadRequest()) .andExpect(jsonPath("$.code") - .value(QuestionPostErrorCode.NOT_ENOUGH_CREDIT.getCode())); + .value(MemberErrorCode.NOT_ENOUGH_CREDIT.getCode())); } @DisplayName("[질문글을 조회할 수 있다.]")