@Api(tags = "게시판 CRUD API")
@RestController
@RequiredArgsConstructor
@RequestMapping("/posts")
public class PostApiController {
    private final PostService postService;
    /**
     * [API] 게시글 등록
     *
     * @return ResponseEntity<Object>: 생성된 게시글 번호 및 응답 코드 반환
     */
    @Operation(summary = "게시글 등록", description = "게시글을 등록 합니다.\n + X-USERID는 3~10자 사이여야 합니다.")
    @ApiResponse(code = 201, message ="INSERT SUCCESS")
    @PostMapping
    public CustomApiResponse<Long> addPost(@Valid @RequestBody CreatePostDto createPostDto, @RequestHeader(name = "X-USERID") @Size(min=3,max = 10) String userId) {
        Long result = postService.addPost(createPostDto, userId);
        return new CustomApiResponse<>(result,SuccessCode.INSERT_SUCCESS);
    }
    /**
     * [API] 게시글 단건 조회
     *
     * @return ResponseEntity<Object>: 조회한 게시글 결과 및 응답 코드 반환
     */
    @Operation(summary = "게시글 단건 조회", description = "입력한 게시글 번호에 대한 게시글 정보를 조회 합니다.")
    @ApiResponse(code = 200, message ="INSERT SUCCESS", response = CustomApiResponse.class)
    @GetMapping("/{postId}")
    public CustomApiResponse<ResponsePostDto> getPost(@PathVariable Long postId) {
        ResponsePostDto result = postService.getPost(postId);
        return new CustomApiResponse<>(result,SuccessCode.SELECT_SUCCESS);
    }
    /**
     * [API] 게시글 수정
     *
     * @return RResponseEntity<Object>: 수정된 게시글 번호 및 응답 코드 반환
     */
    @Operation(summary = "게시글 수정", description = "해당 게시글에 대한 정보를 수정합니다.\n + X-USERID는 3~10자 사이여야 합니다.")
    @ApiResponse(code = 200, message ="INSERT SUCCESS")
    @PatchMapping
    public CustomApiResponse<Long> modifyPost(@Valid @RequestBody UpdatePostDto updatePostDto, @RequestHeader(name = "X-USERID") @Size(min=3,max = 10) String userId) {
        if (updatePostDto.getId() == null) {
            throw new IllegalArgumentException("게시글 번호는 필수 입니다.");
        }
        Long result = postService.modifyPost(updatePostDto, userId);
        return new CustomApiResponse<>(result,SuccessCode.UPDATE_SUCCESS);
    }
    /**
     * [API] 게시글 삭제
     *
     * @return ResponseEntity<ApiResponse<PostDto>>: 삭제된 게시글 번호 및 응답 코드 반환
     */
    @Operation(summary = "게시글 삭제", description = "해당 게시글을 삭제합니다.\n + X-USERID는 3~10자 사이여야 합니다.")
    @ApiResponse(code = 204, message ="INSERT SUCCESS")
    @DeleteMapping("{postId}")
    public CustomApiResponse<Long> deletePost(@PathVariable Long postId, @RequestHeader(name = "X-USERID") @Size(min=3,max = 10) String userId) {
        postService.deletePost(postId, userId);
        return new CustomApiResponse<>(postId,SuccessCode.DELETE_SUCCESS);
    }
    /**
     * [API] 게시글 목록 조회
     *
     * @return ResponseEntity<Object>: 삭제된 게시글 번호 및 응답 코드 반환
     */
    @Operation(summary = "게시글 목록 조회", description = "해당되는 카테고리의 게시글 목록을 조회합니다. - 빈 값일시 모든 게시글을 기준으로 조회")
    @ApiResponse(code = 200, message ="INSERT SUCCESS")
    @GetMapping
    public CustomApiResponse<PageResultDto<ResponsePostListDto>> getPostList(
            @RequestParam(name = "page", defaultValue = "1") int page,
            @RequestParam(name = "size", defaultValue = "5") int size,
            @RequestParam(name = "keyword", required = false) String keyword
    ) {
        PageRequestDto pageRequestDto = new PageRequestDto(page,size,keyword);
        PageResultDto<ResponsePostListDto> result = postService.getPostList(pageRequestDto);
        return new CustomApiResponse<>(result,SuccessCode.SELECT_SUCCESS);
    }
}
@WebMvcTest을 사용했으며 MockMvc를 사용하여 API 엔드포인트의 용청과 응답을 테스트를 진행했다.
Error "jpa metamodel must not be empty"
@WebMvcTest시에는controller,SpringMV레벨의 컴포넌트만 구성되는데 JPA-Auditing 관련 빈이 등록이 안된 상태로 Application 에 있던 @EnableJpaAuditing이 수행되면서 발생하는 에러
@MockBean(JpaMetamodelMappingContext.class)을 Mock 빈을 주입하여 해결
@WebMvcTest(PostApiController.class)
@MockBean(JpaMetamodelMappingContext.class)
class PostApiControllerTest {
    @Autowired
    MockMvc mockMvc;
    @MockBean
    PostService postService;
    @Autowired
    ObjectMapper objectMapper;
    @Nested
    @DisplayName("게시글 등록")
    class addPostControllerTest {
        @DisplayName("성공")
        @Test
        void success() throws Exception {
            //given
            CreatePostDto postDto = CreatePostDto.builder()
                    .name("SpringBoot")
                    .title("게시글 생성")
                    .content("게시글내용")
                    .build();
            String xUserId = "user1";
            Post post = Post.builder()
                            .name("SpringBoot")
                            .title("게시글 생성")
                            .content("게시글내용")
                            .author(xUserId)
                            .build();
            when(postService.addPost(any(CreatePostDto.class), anyString())).thenReturn(post.getId());
            //when
            //then
            mockMvc.perform(post("/posts")
                            .contentType(MediaType.APPLICATION_JSON)
                            .content(objectMapper.writeValueAsString(postDto))
                            .header("X-USERID",xUserId))
                    .andExpect(status().isOk())
                    .andExpect(jsonPath("$.resultMsg").value("INSERT SUCCESS"));
        }
        @DisplayName("실패 - X-USERID 값 null")
        @Test
        void xUserIdIsNull() throws Exception {
            //given
            CreatePostDto postDto = CreatePostDto.builder()
                    .name("SpringBoot")
                    .title("게시글 생성")
                    .content("게시글내용")
                    .build();
            //when
            //then
            mockMvc.perform(post("/posts")
                            .contentType(MediaType.APPLICATION_JSON)
                            .content(objectMapper.writeValueAsString(postDto)))
                    .andExpect(status().isOk())
                    .andExpect(jsonPath("$.message").value("X-USERID 값은 필수 입니다."));
        }
        @DisplayName("실패 - X-USERID 유효성")
        @Test
        void xUserIdInvalid() throws Exception{
            //given
            CreatePostDto postDto = CreatePostDto.builder()
                    .name("SpringBoot")
                    .title("게시글 생성")
                    .content("게시글내용")
                    .build();
            String xUserId = "uuser1asduwqrqrq";
            //when
            //then
            mockMvc.perform(post("/posts")
                            .contentType(MediaType.APPLICATION_JSON)
                            .content(objectMapper.writeValueAsString(postDto))
                            .header("X-USERID", xUserId))
                    .andExpect(status().isOk())
                    .andExpect(jsonPath("$.message").value("X-UERID는 3자에서 10자 사이여야 합니다."));
        }
        @DisplayName("실패 - 게시글 카테고리 null")
        @Test
        void postNameIsNull() throws Exception{
            //given
            CreatePostDto postDto = CreatePostDto.builder()
                    .title("게시글 제목")
                    .content("게시글내용")
                    .build();
            String xUserId = "user1";
            //when
            //then
            mockMvc.perform(post("/posts")
                            .contentType(MediaType.APPLICATION_JSON)
                            .content(objectMapper.writeValueAsString(postDto))
                            .header("X-USERID", xUserId))
                    .andExpect(status().isOk())
                    .andExpect(jsonPath("$.message").value("게시글의 카테고리는 필수 값 입니다."));
        }
        @DisplayName("실패 - 게시글 제목 길이")
        @Test
        void postTitleIsEmpty() throws Exception{
            //given
            String title = "게시글제목";
            title = title.repeat(21);
            CreatePostDto postDto = CreatePostDto.builder()
                    .name("SpringBoot")
                    .title(title)
                    .content("게시글내용")
                    .build();
            String xUserId = "user1";
            //when
            //then
            mockMvc.perform(post("/posts")
                            .contentType(MediaType.APPLICATION_JSON)
                            .content(objectMapper.writeValueAsString(postDto))
                            .header("X-USERID", xUserId))
                    .andExpect(status().isOk())
                    .andExpect(jsonPath("$.message").value("게시글 제목은 1~100자 사이로 작성해 주세요."));
        }
        @DisplayName("실패 - 게시글 제목 길이")
        @Test
        void postTitleIsNull() throws Exception{
            //given
            CreatePostDto postDto = CreatePostDto.builder()
                    .name("SpringBoot")
                    .content("게시글내용")
                    .build();
            String xUserId = "user1";
            //when
            //then
            mockMvc.perform(post("/posts")
                            .contentType(MediaType.APPLICATION_JSON)
                            .content(objectMapper.writeValueAsString(postDto))
                            .header("X-USERID", xUserId))
                    .andExpect(status().isOk())
                    .andExpect(jsonPath("$.message").value("게시글의 제목은 필수 값 입니다."));
        }
    }
    @Nested
    @DisplayName("게시글 수정")
    class modifyPostControllerTest {
        @DisplayName("성공")
        @Test
        void success() throws Exception {
            //given
            String modifiedText = "텍스트 수정";
            UpdatePostDto updatePostDto = UpdatePostDto.builder()
                    .id(1L)
                    .name("Spring")
                    .title("타이틀 변경")
                    .content(modifiedText)
                    .build();
            String xUserId = "user1";
            when(postService.modifyPost(any(),anyString()))
                    .thenReturn(1L);
            //when
            //then
            mockMvc.perform(patch("/posts")
                            .contentType(MediaType.APPLICATION_JSON)
                            .content(objectMapper.writeValueAsString(updatePostDto))
                            .header("X-USERID",xUserId))
                    .andExpect(status().isOk())
                    .andExpect(jsonPath("$.resultMsg").value("UPDATE SUCCESS"));
        }
        @DisplayName("실패 - 게시글번호 null")
        @Test
        void postIdIsNull() throws Exception {
            //given
            UpdatePostDto updatePostDto = UpdatePostDto.builder()
                    .name("SpringBoot")
                    .title("게시글 생성")
                    .content("게시글 내용")
                    .build();
            //when
            //then
            mockMvc.perform(patch("/posts")
                            .contentType(MediaType.APPLICATION_JSON)
                            .content(objectMapper.writeValueAsString(updatePostDto))
                            .header("X-USERID", "user2"))
                    .andExpect(status().isOk())
                    .andExpect(jsonPath("$.message").value("게시글 번호는 필수 값 입니다."));
        }
        @DisplayName("실패 - 작성자 수정자 다름")
        @Test
        void mismatchXUerId() throws Exception{
            //given
            Post post = Post.builder()
                    .name("SpringBoot")
                    .title("게시글 생성")
                    .content("게시글 내용")
                    .author("user1")
                    .build();
            UpdatePostDto updatePostDto = UpdatePostDto.builder()
                    .id(1L)
                    .name("SpringBoot")
                    .title("게시글 생성")
                    .content("게시글 내용")
                    .build();
            when(postService.modifyPost(any(), anyString()))
                    .thenThrow(new BusinessExceptionHandler("작성자와 수정자가 다릅니다.", ErrorCode.FORBIDDEN_ERROR));
            //when
            //then
            mockMvc.perform(patch("/posts")
                            .contentType(MediaType.APPLICATION_JSON)
                            .content(objectMapper.writeValueAsString(updatePostDto))
                            .header("X-USERID", "user2"))
                    .andExpect(status().isOk())
                    .andExpect(jsonPath("$.message").value("작성자와 수정자가 다릅니다."));
        }
    }
    @DisplayName("게시글 삭제")
    @Test
    void deletePost() throws Exception {
        //given
        Long postId = 1L;
        String xUserId = "user1";
        //when
        //then
        mockMvc.perform(delete("/posts/"+postId)
                .contentType(MediaType.APPLICATION_JSON)
                .content(objectMapper.writeValueAsString(postId))
                .header("X-USERID", xUserId))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.resultMsg").value("DELETE SUCCESS"));
    }
    @DisplayName("게시글 목록 조회")
    @Test
    void getPostList() throws Exception {
        //given
        String param = "page=1&size=5&keyword='카테고리'";
        //when
        //then
        mockMvc.perform(get("/posts?"+param)
                .contentType(MediaType.APPLICATION_JSON))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.resultMsg").value("SELECT SUCCESS"));
    }
}
@Api
@Operation
메서드 레벨에 사용되며, 해당 메서드에 대한 기능 설명
summary:작업에 대한 간단한 설명을 제공
description: 작업에 대한 보다 자세한 설명을 제공
@ApiResponse
code: HTTP 응답 코드를 지정message: 응답 코드에 대한 설response 응답의 타입을 지정@NotNull, @Min, @Max, @Size
Swagger 문서에서 API테스트 진행시 해당 어노테이션 기능을 지원해준다고한다.
ex)헤더의 X-USERID의 경우에 3~10자를 만족하지 않을경우 Excute가 실행되지 않음, 아래 이미처럼 붉게 표시되는걸 확인할 수 있다.


큰 도움이 되었습니다, 감사합니다.