Skip to content

Commit

Permalink
문의 사항 작성 API 구현 (#32) (#53)
Browse files Browse the repository at this point in the history
* 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수정
  • Loading branch information
DongkwanKim00 authored Aug 1, 2024
1 parent bbf1459 commit bde3b3d
Show file tree
Hide file tree
Showing 12 changed files with 230 additions and 12 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -41,3 +41,6 @@ out/

### VS Code ###
.vscode/

### Temp Data ###
src/main/resources/data.sql
Original file line number Diff line number Diff line change
@@ -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<QuestionResponseDto> createQuestion(
@Valid @RequestBody QuestionRequestDto dto) {

questionService.createQuestion(dto);

QuestionResponseDto responseDto = new QuestionResponseDto("작성이 완료되었습니다.");
return new ResponseEntity<>(responseDto, HttpStatus.CREATED);
}
}
Original file line number Diff line number Diff line change
@@ -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;

}

Original file line number Diff line number Diff line change
@@ -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;
}
Original file line number Diff line number Diff line change
@@ -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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,7 @@

@Repository
public interface MemberRepository extends JpaRepository<Member, Long> {

Optional<Member> findByIdAndState(Long id, MemberState state);
}

Original file line number Diff line number Diff line change
@@ -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<Question, Long> {
}
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -54,13 +51,13 @@ protected ResponseEntity<ErrorResponse> handleException(final Exception e) {

//validation exception 처리
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<ErrorResponse> processValidationError(MethodArgumentNotValidException e){
public ResponseEntity<ErrorResponse> 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);
}

//잘못된 자료형으로 인한 에러
Expand All @@ -75,13 +72,15 @@ public ResponseEntity<ErrorResponse> methodArgumentTypeMismatchExceptionError(

//잘못된 자료형으로 인한 에러
@ExceptionHandler(HttpMessageNotReadableException.class)
public ResponseEntity<ErrorResponse> handleHttpMessageNotReadable(HttpMessageNotReadableException e) {
final ErrorResponse errorResponse = ErrorResponse.of(ResponseCode.BAD_REQUEST,e);
public ResponseEntity<ErrorResponse> 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<ErrorResponse> httpMediaTypeNotSupportedExceptionError(
Expand Down Expand Up @@ -110,4 +109,5 @@ public ResponseEntity<ErrorResponse> httpServerErrorExceptionError(HttpServerErr
.status(e.getStatusCode())
.body(errorResponse);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
7 changes: 6 additions & 1 deletion src/test/java/kea/enter/enterbe/ControllerTestSupport.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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 {

Expand All @@ -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;

}
11 changes: 11 additions & 0 deletions src/test/java/kea/enter/enterbe/IntegrationTestSupport.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -28,6 +32,9 @@ public abstract class IntegrationTestSupport {

@Autowired
protected ExService exService;

@Autowired
protected QuestionService questionService;
@Autowired
protected ExRepository exRepository;
@Autowired
Expand Down Expand Up @@ -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();
Expand Down
Original file line number Diff line number Diff line change
@@ -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<Question> 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", "[email protected]", "password", "licenseId",
"licensePassword", true, true, 1, MemberRole.USER, MemberState.ACTIVE);
}
}

0 comments on commit bde3b3d

Please sign in to comment.