Skip to content

Commit

Permalink
[feat #23] : 답변 채택 API (#24)
Browse files Browse the repository at this point in the history
* [feat] : 크레딧 증감 로직 추가

* [feat] : 답변 에러코드 추가

* [feat] : 질문글 에러코드 추가

* [refactor] : 질문자 여부 확인 함수 엔티티로 이동

* [feat] : 답변 채택에 따른 상태변경 메서드 엔티티에 추가

* [feat] : 답변 채택 비즈니스 로직 작성

* [test] : Member 파라미터 있는 질문글 fixture 추가

* [test] : 답변 채택 비즈니스 로직 테스트

* [test] : assertAll 검증함수들 통합

* [feat] : 답변 채택 API 함수 작성

* [test] : 답변 채택 API 통합 테스트

* [chore] : swagger 명세

* [style] : 코드 리포멧팅

* [style] : 메서드 네이밍 변경

* [feat] : 엔티티명 및 detail 타입 변경

* [feat] : CreditType enum에 detail 필드 추가

* [remove] : creditDetail enum 삭

* [feat] : 크레딧 에러 코드 member 영역으로 이동

* [feat] : 답변 채택 시 필드 변경 메서드 책임분리

* [feat] : 크레딧 내역 repository 추가

* [feat] : 크레딧 내역 Mapper 추가

* [feat] : 크레딧 내역 저장하는 비즈니스 로직 추가

* [feat] : errorCode 클래스 이동 반영

* [feat] : 크레딧 증감 로직 서비스로 이동 및 크레딧 저장 로직 호출

* [test] : 크레딧 저장 로직 호출 반영

* [test] : 에러 코드 변경 반영

* [test] : 외래키 제약 조건에 따라 creditRepository 먼저 초기화

* [feat] : 필요한 파라미터만 넘기도록 mapper 수정

* [test] : 크레딧 내역 저장 단위 테스트 작성

* [refactor] : DTO 내 팩토리 메서드 삭제

* [fix] : valid 어노테이션 위치 수정

* [style] : 코드 리포멧팅

* [fix] : request에 대한 spring bean validation 반영
  • Loading branch information
hyun2371 authored Aug 9, 2024
1 parent 25b766f commit 0b2758e
Show file tree
Hide file tree
Showing 23 changed files with 392 additions and 71 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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<AnswerDetailResponse> registerAnswer(
@PathVariable Long questionPostId,
Expand All @@ -34,11 +40,24 @@ public ResponseEntity<AnswerDetailResponse> registerAnswer(
return ResponseEntity.ok(response);
}

@Operation(summary = "답변 조회 API", description = "질문글에 속하는 답변을 모두 조회한다.")
@ApiResponse(useReturnTypeSchema = true)
@GetMapping("/{questionPostId}/answers")
public ResponseEntity<PageResponse<AnswerDetailResponse>> getAnswersByQuestionPostId(
@PathVariable Long questionPostId
) {
PageResponse<AnswerDetailResponse> response = answerService.getAnswersByQuestionPostId(questionPostId);
return ResponseEntity.ok(response);
}

@Operation(summary = "답변 채택 API", description = "질문자가 답변을 채택한다.")
@ApiResponse(useReturnTypeSchema = true)
@PostMapping("/answers/{answerId}")
public ResponseEntity<AnswerDetailResponse> getAnswersByQuestionPostId(
@PathVariable Long answerId,
@AuthenticationPrincipal Member member
) {
AnswerDetailResponse response = answerService.chooseAnswer(answerId, member);
return ResponseEntity.ok(response);
}
}
4 changes: 4 additions & 0 deletions src/main/java/com/dnd/gongmuin/answer/domain/Answer.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,4 @@ public record RegisterAnswerRequest(
@NotBlank(message = "답변을 입력해주세요.")
String content
) {
public static RegisterAnswerRequest from(
String content
) {
return new RegisterAnswerRequest(content);
}
}
Original file line number Diff line number Diff line change
@@ -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;
}
48 changes: 42 additions & 6 deletions src/main/java/com/dnd/gongmuin/answer/service/AnswerService.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -25,18 +28,22 @@ 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(
Long questionPostId,
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));
}

Expand All @@ -49,11 +56,40 @@ public PageResponse<AnswerDetailResponse> 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) {
throw new NotFoundException(QuestionPostErrorCode.NOT_FOUND_QUESTION_POST);
}
}

}
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));
}
}
27 changes: 0 additions & 27 deletions src/main/java/com/dnd/gongmuin/credit/CreditDetail.java

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.dnd.gongmuin.credit;
package com.dnd.gongmuin.credit_history;

import static jakarta.persistence.FetchType.*;

Expand All @@ -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;
Expand All @@ -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);
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.dnd.gongmuin.credit;
package com.dnd.gongmuin.credit_history;

import java.util.Arrays;

Expand All @@ -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())
Expand Down
Original file line number Diff line number Diff line change
@@ -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);
}
}
Original file line number Diff line number Diff line change
@@ -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<CreditHistory, Long> {
}
Original file line number Diff line number Diff line change
@@ -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())
));
}
}
13 changes: 13 additions & 0 deletions src/main/java/com/dnd/gongmuin/member/domain/Member.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@
public enum MemberErrorCode implements ErrorCode {

NOT_FOUND_MEMBER("특정 회원을 찾을 수 없습니다.", "MEMBER_001"),
NOT_FOUND_NEW_MEMBER("신규 회원이 아닙니다.", "MEMBER_002");
NOT_FOUND_NEW_MEMBER("신규 회원이 아닙니다.", "MEMBER_002"),
NOT_ENOUGH_CREDIT("보유한 크레딧이 부족합니다.", "MEMBER_003");

private final String message;
private final String code;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand All @@ -27,20 +28,20 @@ public class QuestionPostController {

private final QuestionPostService questionPostService;

@PostMapping
@Operation(summary = "질문글 등록 API", description = "질문글을 등록한다")
@ApiResponse(useReturnTypeSchema = true)
@PostMapping
public ResponseEntity<QuestionPostDetailResponse> 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<QuestionPostDetailResponse> getQuestionPostById(
@PathVariable("questionPostId") Long questionPostId
) {
Expand Down
Loading

0 comments on commit 0b2758e

Please sign in to comment.