diff --git a/src/main/java/com/teamA/hicardi/config/SecurityConfig.java b/src/main/java/com/teamA/hicardi/config/SecurityConfig.java index 868d6ab..621f210 100644 --- a/src/main/java/com/teamA/hicardi/config/SecurityConfig.java +++ b/src/main/java/com/teamA/hicardi/config/SecurityConfig.java @@ -66,6 +66,7 @@ public SecurityFilterChain filterChain(HttpSecurity http, HandlerMappingIntrospe .requestMatchers(mvcMatcherBuilder.pattern("/swagger-ui/**")).permitAll() .requestMatchers(mvcMatcherBuilder.pattern("/swagger-resources/**")).permitAll() .requestMatchers(mvcMatcherBuilder.pattern("/v3/api-docs/**")).permitAll() + .requestMatchers(mvcMatcherBuilder.pattern("/faq")).permitAll() .anyRequest().authenticated()) .addFilterAfter(customJsonUsernamePasswordAuthenticationFilter(), LogoutFilter.class) .addFilterBefore(jwtAuthenticationProcessingFilter(), CustomJsonAuthenticationFilter.class) diff --git a/src/main/java/com/teamA/hicardi/domain/faq/controller/FaqController.java b/src/main/java/com/teamA/hicardi/domain/faq/controller/FaqController.java new file mode 100644 index 0000000..c50b07a --- /dev/null +++ b/src/main/java/com/teamA/hicardi/domain/faq/controller/FaqController.java @@ -0,0 +1,40 @@ +package com.teamA.hicardi.domain.faq.controller; + +import com.teamA.hicardi.common.dto.ResponseDto; +import com.teamA.hicardi.domain.faq.dto.request.FaqSaveRequestDto; +import com.teamA.hicardi.domain.faq.service.FaqService; +import com.teamA.hicardi.error.dto.ErrorResponse; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.security.SecurityRequirement; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +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 +@RequiredArgsConstructor +@RequestMapping("/faq") +public class FaqController { + + private final FaqService faqService; + + @Operation(summary = "FAQ 저장", description = "FAQ를 저장합니다.", + security = { @SecurityRequirement(name = "bearer-key") }, + responses = { + @ApiResponse(responseCode = "204", description = "FAQ 저장 성공") + , @ApiResponse(responseCode = "400", description = "1. 잘못된 카테고리입니다. \t\n 2. 질문은 필수 입력 값입니다. \t\n 3. 답변은 필수 입력 값입니다.", content = @Content(schema = @Schema(implementation = ErrorResponse.class))) + }) + @PostMapping + public ResponseEntity saveFaq(@RequestBody @Valid FaqSaveRequestDto requestDto) { + faqService.saveFaq(requestDto); + return ResponseDto.noContent(); + } + + +} diff --git a/src/main/java/com/teamA/hicardi/domain/faq/dto/request/FaqSaveRequestDto.java b/src/main/java/com/teamA/hicardi/domain/faq/dto/request/FaqSaveRequestDto.java new file mode 100644 index 0000000..48cd353 --- /dev/null +++ b/src/main/java/com/teamA/hicardi/domain/faq/dto/request/FaqSaveRequestDto.java @@ -0,0 +1,16 @@ +package com.teamA.hicardi.domain.faq.dto.request; + +import com.teamA.hicardi.domain.faq.entity.Category; +import com.teamA.hicardi.domain.faq.entity.Faq; +import jakarta.validation.constraints.NotBlank; + +public record FaqSaveRequestDto(String category, @NotBlank(message = "질문은 필수 입력 값입니다.") String question, @NotBlank(message = "답변은 필수 입력 값입니다.") String answer) { + + public Faq toEntity(Category category, String question, String answer) { + return Faq.builder() + .category(category) + .question(question) + .answer(answer) + .build(); + } +} diff --git a/src/main/java/com/teamA/hicardi/domain/faq/entity/Category.java b/src/main/java/com/teamA/hicardi/domain/faq/entity/Category.java index fa20fa9..960d824 100644 --- a/src/main/java/com/teamA/hicardi/domain/faq/entity/Category.java +++ b/src/main/java/com/teamA/hicardi/domain/faq/entity/Category.java @@ -1,5 +1,19 @@ package com.teamA.hicardi.domain.faq.entity; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.teamA.hicardi.error.ErrorCode; +import com.teamA.hicardi.error.exception.custom.BusinessException; + +import java.util.stream.Stream; + public enum Category { - BATTERY, EXERCISE // 추가 예정 + BATTERY, EXERCISE; // 추가 예정 + + @JsonCreator + public static Category create(String requestValue) { + return Stream.of(values()) + .filter(v -> v.toString().equalsIgnoreCase(requestValue)) + .findFirst() + .orElseThrow(() -> new BusinessException(ErrorCode.INVALID_CATEGORY)); + } } diff --git a/src/main/java/com/teamA/hicardi/domain/faq/entity/Faq.java b/src/main/java/com/teamA/hicardi/domain/faq/entity/Faq.java index 6070cfc..4789fb8 100644 --- a/src/main/java/com/teamA/hicardi/domain/faq/entity/Faq.java +++ b/src/main/java/com/teamA/hicardi/domain/faq/entity/Faq.java @@ -2,10 +2,7 @@ import com.teamA.hicardi.common.entity.BaseTimeEntity; import jakarta.persistence.*; -import lombok.AccessLevel; -import lombok.AllArgsConstructor; -import lombok.Getter; -import lombok.NoArgsConstructor; +import lombok.*; @Getter @NoArgsConstructor(access = AccessLevel.PROTECTED) @@ -23,4 +20,11 @@ public class Faq extends BaseTimeEntity { private String question; private String answer; + + @Builder + public Faq(Category category, String question, String answer) { + this.category = category; + this.question = question; + this.answer = answer; + } } diff --git a/src/main/java/com/teamA/hicardi/domain/faq/repository/FaqRepository.java b/src/main/java/com/teamA/hicardi/domain/faq/repository/FaqRepository.java new file mode 100644 index 0000000..1af57aa --- /dev/null +++ b/src/main/java/com/teamA/hicardi/domain/faq/repository/FaqRepository.java @@ -0,0 +1,7 @@ +package com.teamA.hicardi.domain.faq.repository; + +import com.teamA.hicardi.domain.faq.entity.Faq; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface FaqRepository extends JpaRepository { +} diff --git a/src/main/java/com/teamA/hicardi/domain/faq/service/FaqService.java b/src/main/java/com/teamA/hicardi/domain/faq/service/FaqService.java new file mode 100644 index 0000000..01adf44 --- /dev/null +++ b/src/main/java/com/teamA/hicardi/domain/faq/service/FaqService.java @@ -0,0 +1,25 @@ +package com.teamA.hicardi.domain.faq.service; + +import com.teamA.hicardi.domain.faq.dto.request.FaqSaveRequestDto; +import com.teamA.hicardi.domain.faq.entity.Category; +import com.teamA.hicardi.domain.faq.entity.Faq; +import com.teamA.hicardi.domain.faq.repository.FaqRepository; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Slf4j +@Service +@Transactional +@RequiredArgsConstructor +public class FaqService { + + private final FaqRepository faqRepository; + + public void saveFaq(FaqSaveRequestDto requestDto) { + Category category = Category.create(requestDto.category()); + Faq faq = requestDto.toEntity(category, requestDto.question(), requestDto.answer()); + faqRepository.save(faq); + } +} diff --git a/src/main/java/com/teamA/hicardi/error/ErrorCode.java b/src/main/java/com/teamA/hicardi/error/ErrorCode.java index fb98a5e..2c76d30 100644 --- a/src/main/java/com/teamA/hicardi/error/ErrorCode.java +++ b/src/main/java/com/teamA/hicardi/error/ErrorCode.java @@ -15,11 +15,13 @@ public enum ErrorCode { ALREADY_EXIST_EMAIL(BAD_REQUEST, "이미 존재하는 이메일입니다."), ALREADY_EXIST_USERID(BAD_REQUEST, "이미 존재하는 아이디입니다."), INVALID_FILE_UPLOAD(BAD_REQUEST, "파일 업로드에 실패하였습니다."), + INVALID_CATEGORY(BAD_REQUEST, "잘못된 카테고리입니다."), INVALID_TOKEN(UNAUTHORIZED, "잘못된 토큰입니다."), CART_NOT_FOUND(NOT_FOUND, "해당 장바구니를 찾을 수 없습니다."), ITEM_NOT_FOUND(NOT_FOUND, "해당 상품을 찾을 수 없습니다."); + private final int code; private final String message; diff --git a/src/main/java/com/teamA/hicardi/error/exception/GlobalExceptionHandler.java b/src/main/java/com/teamA/hicardi/error/exception/GlobalExceptionHandler.java index 2bcb510..56c5b0b 100644 --- a/src/main/java/com/teamA/hicardi/error/exception/GlobalExceptionHandler.java +++ b/src/main/java/com/teamA/hicardi/error/exception/GlobalExceptionHandler.java @@ -3,11 +3,19 @@ import com.teamA.hicardi.error.exception.custom.BusinessException; import com.teamA.hicardi.error.exception.custom.TokenException; import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; +import org.springframework.validation.BindingResult; +import org.springframework.web.bind.MethodArgumentNotValidException; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; import com.teamA.hicardi.error.dto.ErrorResponse; +import java.util.List; +import java.util.stream.Collectors; + +import static org.springframework.http.HttpStatus.BAD_REQUEST; + @Slf4j @RestControllerAdvice public class GlobalExceptionHandler { @@ -30,4 +38,14 @@ public ResponseEntity handle(final TokenException e) { return ResponseEntity.status(e.getCode()).body(ErrorResponse.of(e.getCode(), e.getMessage())); } + @ExceptionHandler + public ResponseEntity handle(MethodArgumentNotValidException e) { + BindingResult bindingResult = e.getBindingResult(); + String firstErrorMessage = bindingResult.getFieldErrors().get(0).getDefaultMessage(); + + List errorList = bindingResult.getFieldErrors().stream().map(err -> err.getDefaultMessage()).collect(Collectors.toList()); + log.warn("MethodArgumentNotValidExceptionException = {}", errorList); + + return ResponseEntity.status(BAD_REQUEST).body(ErrorResponse.of(BAD_REQUEST.value(), firstErrorMessage)); + } } \ No newline at end of file diff --git a/src/test/java/com/teamA/hicardi/domain/faq/controller/FaqControllerTest.java b/src/test/java/com/teamA/hicardi/domain/faq/controller/FaqControllerTest.java new file mode 100644 index 0000000..5c139e6 --- /dev/null +++ b/src/test/java/com/teamA/hicardi/domain/faq/controller/FaqControllerTest.java @@ -0,0 +1,57 @@ +package com.teamA.hicardi.domain.faq.controller; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.teamA.hicardi.domain.faq.dto.request.FaqSaveRequestDto; +import com.teamA.hicardi.domain.faq.service.FaqService; +import org.junit.jupiter.api.BeforeEach; +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 org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.ResultActions; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import org.springframework.web.filter.CharacterEncodingFilter; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@ExtendWith(MockitoExtension.class) +class FaqControllerTest { + + @InjectMocks + private FaqController faqController; + @Mock + private FaqService faqService; + private ObjectMapper objectMapper = new ObjectMapper(); + private MockMvc mockMvc; + + @BeforeEach + void setUp() { + mockMvc = MockMvcBuilders.standaloneSetup(faqController) + .addFilter(new CharacterEncodingFilter("UTF-8", true)) + .build(); + } + + @Test + void FAQ_생성() throws Exception { + //given + FaqSaveRequestDto requestDto = new FaqSaveRequestDto("BATTERY", "질문입니다.", "답변입니다."); + + //when + ResultActions result = mockMvc.perform( + post("/faq") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(requestDto)) + ); + + //then + result.andExpect(status().isNoContent()); + verify(faqService, times(1)).saveFaq(any()); + } +} \ No newline at end of file