Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Feature] FAQ 생성 기능 구현 #36

Merged
merged 4 commits into from
Sep 8, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/main/java/com/teamA/hicardi/config/SecurityConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

좋아요.

Original file line number Diff line number Diff line change
@@ -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<Void> saveFaq(@RequestBody @Valid FaqSaveRequestDto requestDto) {
faqService.saveFaq(requestDto);
return ResponseDto.noContent();
}


}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

좋습니다.

Original file line number Diff line number Diff line change
@@ -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();
}
}
16 changes: 15 additions & 1 deletion src/main/java/com/teamA/hicardi/domain/faq/entity/Category.java
Original file line number Diff line number Diff line change
@@ -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));
}
}
12 changes: 8 additions & 4 deletions src/main/java/com/teamA/hicardi/domain/faq/entity/Faq.java
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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;
}
}
Original file line number Diff line number Diff line change
@@ -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<Faq, Long> {
}
25 changes: 25 additions & 0 deletions src/main/java/com/teamA/hicardi/domain/faq/service/FaqService.java
Original file line number Diff line number Diff line change
@@ -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);
}
}
2 changes: 2 additions & 0 deletions src/main/java/com/teamA/hicardi/error/ErrorCode.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -30,4 +38,14 @@ public ResponseEntity<ErrorResponse> handle(final TokenException e) {
return ResponseEntity.status(e.getCode()).body(ErrorResponse.of(e.getCode(), e.getMessage()));
}

@ExceptionHandler
public ResponseEntity<ErrorResponse> handle(MethodArgumentNotValidException e) {
BindingResult bindingResult = e.getBindingResult();
String firstErrorMessage = bindingResult.getFieldErrors().get(0).getDefaultMessage();

List<String> 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));
}
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

굿 !

Original file line number Diff line number Diff line change
@@ -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());
}
}
Loading