From bde3b3d90ca3af6841754176edd27d667d24a348 Mon Sep 17 00:00:00 2001 From: DongkwanKim00 <112566149+DongkwanKim00@users.noreply.github.com> Date: Thu, 1 Aug 2024 09:25:11 +0900 Subject: [PATCH] =?UTF-8?q?=EB=AC=B8=EC=9D=98=20=EC=82=AC=ED=95=AD=20?= =?UTF-8?q?=EC=9E=91=EC=84=B1=20API=20=EA=B5=AC=ED=98=84=20(#32)=20(#53)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Feat: 문의사항 작성 기능 구현 (#32) - 사용자가 문의사항 작성 시 DB에 저장 기능 구현 * Feat: 문의사항 작성에 대한 예외처리(#32) - 작성자 ID가 없는 경우 - 본문 내용이 없는 경우 - 카테고리 선택을 안 한 경우 * Fix: Response 내용 변경 (#32) - Response Dto 추가 * Feat: 문의사항 테스트 코드 작성 (#32) - 문의사항 저장 기능 - 존재하는 멤버 ID인지 검사 * Feat: Swagger 추가 (#32) * Fix: PR message 수정 (#32) - URI 수정 - @Parameters를 @Schema로 수정 - Member검사할 때 state 조건 수정 - question폴더 내부 ExampleDto파일 삭제 - gitignore수정 --- .gitignore | 3 + .../controller/QuestionController.java | 45 ++++++++++++ .../dto/request/QuestionRequestDto.java | 25 +++++++ .../dto/response/QuestionResponseDto.java | 11 +++ .../api/question/service/QuestionService.java | 33 +++++++++ .../member/repository/MemberRepository.java | 2 + .../repository/QuestionRepository.java | 9 +++ .../exception/GlobalExceptionHandler.java | 22 +++--- .../global/common/exception/ResponseCode.java | 5 ++ .../enter/enterbe/ControllerTestSupport.java | 7 +- .../enter/enterbe/IntegrationTestSupport.java | 11 +++ .../question/service/QuestionServiceTest.java | 69 +++++++++++++++++++ 12 files changed, 230 insertions(+), 12 deletions(-) create mode 100644 src/main/java/kea/enter/enterbe/api/question/controller/QuestionController.java create mode 100644 src/main/java/kea/enter/enterbe/api/question/controller/dto/request/QuestionRequestDto.java create mode 100644 src/main/java/kea/enter/enterbe/api/question/controller/dto/response/QuestionResponseDto.java create mode 100644 src/main/java/kea/enter/enterbe/api/question/service/QuestionService.java create mode 100644 src/main/java/kea/enter/enterbe/domain/question/repository/QuestionRepository.java create mode 100644 src/test/java/kea/enter/enterbe/api/question/service/QuestionServiceTest.java diff --git a/.gitignore b/.gitignore index 28ea54c4..56bb810a 100644 --- a/.gitignore +++ b/.gitignore @@ -41,3 +41,6 @@ out/ ### VS Code ### .vscode/ + +### Temp Data ### +src/main/resources/data.sql diff --git a/src/main/java/kea/enter/enterbe/api/question/controller/QuestionController.java b/src/main/java/kea/enter/enterbe/api/question/controller/QuestionController.java new file mode 100644 index 00000000..adc4b055 --- /dev/null +++ b/src/main/java/kea/enter/enterbe/api/question/controller/QuestionController.java @@ -0,0 +1,45 @@ +package kea.enter.enterbe.api.question.controller; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.Parameters; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import kea.enter.enterbe.api.question.controller.dto.request.QuestionRequestDto; +import kea.enter.enterbe.api.question.controller.dto.response.QuestionResponseDto; +import kea.enter.enterbe.api.question.service.QuestionService; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/questions") +@RequiredArgsConstructor +@Tag(name = "문의사항 작성", description = "문의사항 작성 API") +public class QuestionController { + + private final QuestionService questionService; + + @Operation(summary = "사용자가 작성한 문의사항 저장") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "작성이 완료되었습니다.", content = @Content(mediaType = "application/json")), + @ApiResponse(responseCode = "MEM-ERR-001", description = "멤버가 존재하지 않습니다.", content = @Content(mediaType = "application/json")), + @ApiResponse(responseCode = "GLB-ERR-001", description = "필수 입력칸이 입력되지 않았습니다.", content = @Content(mediaType = "application/json")), + }) + @PostMapping + public ResponseEntity createQuestion( + @Valid @RequestBody QuestionRequestDto dto) { + + questionService.createQuestion(dto); + + QuestionResponseDto responseDto = new QuestionResponseDto("작성이 완료되었습니다."); + return new ResponseEntity<>(responseDto, HttpStatus.CREATED); + } +} diff --git a/src/main/java/kea/enter/enterbe/api/question/controller/dto/request/QuestionRequestDto.java b/src/main/java/kea/enter/enterbe/api/question/controller/dto/request/QuestionRequestDto.java new file mode 100644 index 00000000..2b6c7e54 --- /dev/null +++ b/src/main/java/kea/enter/enterbe/api/question/controller/dto/request/QuestionRequestDto.java @@ -0,0 +1,25 @@ +package kea.enter.enterbe.api.question.controller.dto.request; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import kea.enter.enterbe.domain.question.entity.QuestionCategory; +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public class QuestionRequestDto { + + @NotNull(message = "멤버 아이디를 입력해야 합니다.") + @Schema(description = "멤버 ID", example = "2") + private Long memberId; + @NotBlank(message = "내용을 입력해야 합니다.") + @Schema(description = "문의사항 내용", example = "추첨 날짜는 언제인가요?") + private String content; + @NotNull(message = "카테고리를 입력해야 합니다.") + @Schema(description = "문의사항 카테고리(USER, SERVICE, VEHICLE, ETC)", example = "USER") + private QuestionCategory category; + +} + diff --git a/src/main/java/kea/enter/enterbe/api/question/controller/dto/response/QuestionResponseDto.java b/src/main/java/kea/enter/enterbe/api/question/controller/dto/response/QuestionResponseDto.java new file mode 100644 index 00000000..f29d6192 --- /dev/null +++ b/src/main/java/kea/enter/enterbe/api/question/controller/dto/response/QuestionResponseDto.java @@ -0,0 +1,11 @@ +package kea.enter.enterbe.api.question.controller.dto.response; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public class QuestionResponseDto { + + private String message; +} diff --git a/src/main/java/kea/enter/enterbe/api/question/service/QuestionService.java b/src/main/java/kea/enter/enterbe/api/question/service/QuestionService.java new file mode 100644 index 00000000..41385558 --- /dev/null +++ b/src/main/java/kea/enter/enterbe/api/question/service/QuestionService.java @@ -0,0 +1,33 @@ +package kea.enter.enterbe.api.question.service; + +import kea.enter.enterbe.api.question.controller.dto.request.QuestionRequestDto; +import kea.enter.enterbe.domain.member.entity.Member; +import kea.enter.enterbe.domain.member.entity.MemberState; +import kea.enter.enterbe.domain.member.repository.MemberRepository; +import kea.enter.enterbe.domain.question.entity.Question; +import kea.enter.enterbe.domain.question.entity.QuestionState; +import kea.enter.enterbe.domain.question.repository.QuestionRepository; +import kea.enter.enterbe.global.common.exception.CustomException; +import kea.enter.enterbe.global.common.exception.ResponseCode; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +public class QuestionService { + + private final QuestionRepository questionRepository; + private final MemberRepository memberRepository; + + @Transactional + public void createQuestion(QuestionRequestDto dto) { + Member member = memberRepository.findByIdAndState(dto.getMemberId(), MemberState.ACTIVE) + .orElseThrow(() -> new CustomException(ResponseCode.NOT_FOUND_MEMBER)); + + // state는 작성시에 WAIT로 기본값 고정 + Question question = Question.of(member, dto.getContent(), dto.getCategory(), + QuestionState.WAIT); + questionRepository.save(question); + } +} diff --git a/src/main/java/kea/enter/enterbe/domain/member/repository/MemberRepository.java b/src/main/java/kea/enter/enterbe/domain/member/repository/MemberRepository.java index 0989cf50..b943f31d 100644 --- a/src/main/java/kea/enter/enterbe/domain/member/repository/MemberRepository.java +++ b/src/main/java/kea/enter/enterbe/domain/member/repository/MemberRepository.java @@ -8,5 +8,7 @@ @Repository public interface MemberRepository extends JpaRepository { + Optional findByIdAndState(Long id, MemberState state); } + diff --git a/src/main/java/kea/enter/enterbe/domain/question/repository/QuestionRepository.java b/src/main/java/kea/enter/enterbe/domain/question/repository/QuestionRepository.java new file mode 100644 index 00000000..40c305d5 --- /dev/null +++ b/src/main/java/kea/enter/enterbe/domain/question/repository/QuestionRepository.java @@ -0,0 +1,9 @@ +package kea.enter.enterbe.domain.question.repository; + +import kea.enter.enterbe.domain.question.entity.Question; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface QuestionRepository extends JpaRepository { +} \ No newline at end of file diff --git a/src/main/java/kea/enter/enterbe/global/common/exception/GlobalExceptionHandler.java b/src/main/java/kea/enter/enterbe/global/common/exception/GlobalExceptionHandler.java index f1b9f138..368f19dc 100644 --- a/src/main/java/kea/enter/enterbe/global/common/exception/GlobalExceptionHandler.java +++ b/src/main/java/kea/enter/enterbe/global/common/exception/GlobalExceptionHandler.java @@ -1,10 +1,7 @@ package kea.enter.enterbe.global.common.exception; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.validation.ValidationException; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.http.converter.HttpMessageNotReadableException; import org.springframework.web.HttpMediaTypeNotSupportedException; @@ -54,13 +51,13 @@ protected ResponseEntity handleException(final Exception e) { //validation exception 처리 @ExceptionHandler(MethodArgumentNotValidException.class) - public ResponseEntity processValidationError(MethodArgumentNotValidException e){ + public ResponseEntity processValidationError(MethodArgumentNotValidException e) { log.error("processValidationError: {}", e.getMessage()); final ErrorResponse errorResponse = ErrorResponse.of(ResponseCode.BAD_REQUEST, - e.getBindingResult().getAllErrors().get(0).getDefaultMessage()); + e.getBindingResult().getAllErrors().get(0).getDefaultMessage()); return ResponseEntity - .status(e.getStatusCode()) - .body(errorResponse); + .status(e.getStatusCode()) + .body(errorResponse); } //잘못된 자료형으로 인한 에러 @@ -75,13 +72,15 @@ public ResponseEntity methodArgumentTypeMismatchExceptionError( //잘못된 자료형으로 인한 에러 @ExceptionHandler(HttpMessageNotReadableException.class) - public ResponseEntity handleHttpMessageNotReadable(HttpMessageNotReadableException e) { - final ErrorResponse errorResponse = ErrorResponse.of(ResponseCode.BAD_REQUEST,e); + public ResponseEntity handleHttpMessageNotReadable( + HttpMessageNotReadableException e) { + final ErrorResponse errorResponse = ErrorResponse.of(ResponseCode.BAD_REQUEST, e); return ResponseEntity - .status(ResponseCode.BAD_REQUEST.getStatus()) - .body(errorResponse); + .status(ResponseCode.BAD_REQUEST.getStatus()) + .body(errorResponse); } + //지원하지 않는 media type 에러 @ExceptionHandler(HttpMediaTypeNotSupportedException.class) public ResponseEntity httpMediaTypeNotSupportedExceptionError( @@ -110,4 +109,5 @@ public ResponseEntity httpServerErrorExceptionError(HttpServerErr .status(e.getStatusCode()) .body(errorResponse); } + } \ No newline at end of file diff --git a/src/main/java/kea/enter/enterbe/global/common/exception/ResponseCode.java b/src/main/java/kea/enter/enterbe/global/common/exception/ResponseCode.java index 0c016d55..8b932fe7 100644 --- a/src/main/java/kea/enter/enterbe/global/common/exception/ResponseCode.java +++ b/src/main/java/kea/enter/enterbe/global/common/exception/ResponseCode.java @@ -26,8 +26,13 @@ public enum ResponseCode { BAD_REQUEST("GLB-ERR-001", HttpStatus.NOT_FOUND, "잘못된 요청입니다."), METHOD_NOT_ALLOWED("GLB-ERR-002", HttpStatus.METHOD_NOT_ALLOWED, "허용되지 않은 메서드입니다."), INTERNAL_SERVER_ERROR("GLB-ERR-003", HttpStatus.INTERNAL_SERVER_ERROR, "내부 서버 오류입니다."), + + // NOTICE + NOT_FOUND_MEMBER("MEM-ERR-001", HttpStatus.NOT_FOUND, "멤버가 존재하지 않습니다."), + NOT_IMAGE_FILE("GLB-ERR-004", HttpStatus.BAD_REQUEST, "이미지 파일이 아닙니다."); + private final String code; private final HttpStatus status; private final String message; diff --git a/src/test/java/kea/enter/enterbe/ControllerTestSupport.java b/src/test/java/kea/enter/enterbe/ControllerTestSupport.java index 50eca0e3..88aef095 100644 --- a/src/test/java/kea/enter/enterbe/ControllerTestSupport.java +++ b/src/test/java/kea/enter/enterbe/ControllerTestSupport.java @@ -2,6 +2,8 @@ import com.fasterxml.jackson.databind.ObjectMapper; import kea.enter.enterbe.api.controller.ex.ExController; +import kea.enter.enterbe.api.question.controller.QuestionController; +import kea.enter.enterbe.api.question.service.QuestionService; import kea.enter.enterbe.api.penalty.controller.AdminPenaltyController; import kea.enter.enterbe.api.penalty.service.AdminPenaltyService; import kea.enter.enterbe.api.service.ex.ExService; @@ -16,7 +18,7 @@ import org.springframework.test.web.servlet.MockMvc; @WebMvcTest(controllers = { - ExController.class, SecurityConfig.class, VehicleController.class, AdminPenaltyController.class + ExController.class, SecurityConfig.class, VehicleController.class, AdminPenaltyController.class, QuestionController.class }) public abstract class ControllerTestSupport { @@ -27,9 +29,12 @@ public abstract class ControllerTestSupport { @MockBean protected ExService exService; @MockBean + protected QuestionService questionService; + @MockBean protected VehicleService vehicleService; @MockBean protected FileUtil fileUtil; @MockBean protected AdminPenaltyService adminPenaltyService; + } diff --git a/src/test/java/kea/enter/enterbe/IntegrationTestSupport.java b/src/test/java/kea/enter/enterbe/IntegrationTestSupport.java index 9b2aeabe..f157657a 100644 --- a/src/test/java/kea/enter/enterbe/IntegrationTestSupport.java +++ b/src/test/java/kea/enter/enterbe/IntegrationTestSupport.java @@ -3,11 +3,15 @@ import java.time.Clock; import kea.enter.enterbe.api.penalty.service.AdminPenaltyService; import kea.enter.enterbe.api.service.ex.ExService; +import kea.enter.enterbe.api.question.service.QuestionService; import kea.enter.enterbe.api.vehicle.service.AdminVehicleService; import kea.enter.enterbe.api.vehicle.service.VehicleService; import kea.enter.enterbe.domain.apply.repository.ApplyRepository; import kea.enter.enterbe.domain.ex.repository.ExRepository; import kea.enter.enterbe.domain.member.repository.MemberRepository; +import kea.enter.enterbe.domain.question.repository.QuestionRepository; +import kea.enter.enterbe.api.vehicle.service.VehicleService; +import kea.enter.enterbe.domain.apply.repository.ApplyRepository; import kea.enter.enterbe.domain.note.repository.VehicleNoteRepository; import kea.enter.enterbe.domain.penalty.repository.PenaltyRepository; import kea.enter.enterbe.domain.report.repository.VehicleReportRepository; @@ -28,6 +32,9 @@ public abstract class IntegrationTestSupport { @Autowired protected ExService exService; + + @Autowired + protected QuestionService questionService; @Autowired protected ExRepository exRepository; @Autowired @@ -63,9 +70,13 @@ public abstract class IntegrationTestSupport { @MockBean protected AdminVehicleService adminVehicleService; + @Autowired + protected QuestionRepository questionRepository; + @AfterEach void tearDown() { exRepository.deleteAllInBatch(); + questionRepository.deleteAllInBatch(); vehicleNoteRepository.deleteAllInBatch(); vehicleReportRepository.deleteAllInBatch(); winningRepository.deleteAllInBatch(); diff --git a/src/test/java/kea/enter/enterbe/api/question/service/QuestionServiceTest.java b/src/test/java/kea/enter/enterbe/api/question/service/QuestionServiceTest.java new file mode 100644 index 00000000..3136172c --- /dev/null +++ b/src/test/java/kea/enter/enterbe/api/question/service/QuestionServiceTest.java @@ -0,0 +1,69 @@ +package kea.enter.enterbe.api.question.service; + +import kea.enter.enterbe.IntegrationTestSupport; +import kea.enter.enterbe.api.question.controller.dto.request.QuestionRequestDto; +import kea.enter.enterbe.domain.member.entity.Member; +import kea.enter.enterbe.domain.member.entity.MemberRole; +import kea.enter.enterbe.domain.member.entity.MemberState; +import kea.enter.enterbe.domain.question.entity.Question; +import kea.enter.enterbe.domain.question.entity.QuestionCategory; +import kea.enter.enterbe.global.common.exception.CustomException; +import kea.enter.enterbe.global.common.exception.ResponseCode; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.tuple; + +public class QuestionServiceTest extends IntegrationTestSupport { + + @DisplayName(value = "문의사항을 저장한다") + @Test + public void testCreateQuestion_Success() { + String questionContentTest = "문의사항 테스트 문장"; + Member member = createMember(); + memberRepository.save(member); + + // given + QuestionRequestDto requestDto = new QuestionRequestDto(member.getId(), questionContentTest, + QuestionCategory.USER); + + // when + questionService.createQuestion(requestDto); + + //then + List questionList = questionRepository.findAll(); + assertThat(questionList).hasSize(1) + .extracting("member.id", "content", "category") + .containsExactlyInAnyOrder( + tuple(member.getId(), questionContentTest, QuestionCategory.USER) + ); + } + + @DisplayName(value = "존재하는 멤버 ID인지 검사한다") + @Test + public void testCreateQuestion_MemberNotFound() { + Long memberIdTest = 1L; + String questionContentTest = "문의사항 테스트 문장"; + Member member = createMember(); + memberRepository.save(member); + + // given + QuestionRequestDto requestDto = new QuestionRequestDto(memberIdTest, questionContentTest, + QuestionCategory.USER); + + // when & then + CustomException exception = assertThrows(CustomException.class, () -> { + questionService.createQuestion(requestDto); + }); + + assertThat(exception.getResponseCode()).isEqualTo(ResponseCode.NOT_FOUND_MEMBER); + } + + private Member createMember() { + return Member.of("2", "name", "test@naver.com", "password", "licenseId", + "licensePassword", true, true, 1, MemberRole.USER, MemberState.ACTIVE); + } +}