반응형

이번 시간에는 게시글 조회 API를 만들어보겠습니다.

 

먼저 repository.post.PostRepository에 다음과 같은 쿼리를 작성해줍니다.

package kukekyakya.kukemarket.repository.post;

...

public interface PostRepository extends JpaRepository<Post, Long> {

    @Query("select p from Post p join fetch p.member where p.id = :id")
    Optional<Post> findByIdWithMember(Long id);
}

각 게시글을 조회할 때는 작성자의 정보도 보내줄 것이기 때문에, fetch join을 이용하여 Member도 함께 조회해줍니다.

 

 

작성된 쿼리를 테스트해봅시다.

PostRepositoryTest에 새로운 테스트를 작성해줍니다.

// PostRepositoryTest.java

@Test
void findByIdWithMemberTest() {
    // given
    Post post = postRepository.save(createPost(member, category));

    // when
    Post foundPost = postRepository.findByIdWithMember(post.getId()).orElseThrow(PostNotFoundException::new);

    // then
    Member foundMember = foundPost.getMember();
    assertThat(foundMember.getEmail()).isEqualTo(member.getEmail());
}

로그를 확인해보면, 1개의 쿼리만 나가는 것을 확인할 수 있습니다.

 

 

이제 service.post.PostService에 read 메소드를 작성해주겠습니다.

// PostService.java
public PostDto read(Long id) {
    return PostDto.toDto(postRepository.findById(id).orElseThrow(PostNotFoundException::new));
}

단순히 파라미터로 전달 받은 id를 이용하여 조회된 Post를, PostDto로 변환하여 반환해줍니다.

 

 

dto.post.PostDto는 다음과 같습니다.

package kukekyakya.kukemarket.dto.post;

import ...

@Data
@AllArgsConstructor
public class PostDto {
    private Long id;
    private String title;
    private String content;
    private Long price;
    private MemberDto member;
    private List<ImageDto> images;

    public static PostDto toDto(Post post) {
        return new PostDto(
                post.getId(),
                post.getTitle(),
                post.getContent(),
                post.getPrice(),
                MemberDto.toDto(post.getMember()),
                post.getImages().stream().map(i -> ImageDto.toDto(i)).collect(toList())
        );
    }
}

작성자의 정보도 함께 가지고 있습니다.

각 게시글이 가지고 있는 이미지에 대한 정보도 반환해줍니다.

 

 

이번에는 dto.post.ImageDto를 살펴보겠습니다.

package kukekyakya.kukemarket.dto.post;

import ...

@Data
@AllArgsConstructor
public class ImageDto {
    private Long id;
    private String originName;
    private String uniqueName;
    public static ImageDto toDto(Image image) {
        return new ImageDto(image.getId(), image.getOriginName(), image.getUniqueName());
    }
}

원래의 파일 명과 서버에서 생성한 고유한 파일 명으로 DTO를 생성해서 반환해줍니다.

 

 

이제 PostService에 새롭게 작성된 read 메소드에 대해 테스트해봅시다.

PostServiceTest에 다음과 같은 두 개의 테스트를 추가해줍니다.

// PostServiceTest.java
@Test
void readTest() {
    // given
    Post post = createPostWithImages(List.of(createImage(), createImage()));
    given(postRepository.findById(anyLong())).willReturn(Optional.of(post));

    // when
    PostDto postDto = postService.read(1L);

    // then
    assertThat(postDto.getTitle()).isEqualTo(post.getTitle());
    assertThat(postDto.getImages().size()).isEqualTo(post.getImages().size());
}

@Test
void readExceptionByPostNotFoundTest() {
    // given
    given(postRepository.findById(anyLong())).willReturn(Optional.ofNullable(null));

    // when, then
    assertThatThrownBy(() -> postService.read(1L)).isInstanceOf(PostNotFoundException.class);
}

정상적인 조회와 없는 게시글 조회에 대해서 테스트해주었습니다.

자세한 설명은 생략하겠습니다.

 

 

 

다음으로 controller.post.PostController에 read를 수행하기 위한 웹 API를 추가해줍니다.

// PostController.java
@ApiOperation(value = "게시글 조회", notes = "게시글을 조회한다.")
@GetMapping("/api/posts/{id}")
@ResponseStatus(HttpStatus.OK)
public Response read(@ApiParam(value = "게시글 id", required = true) @PathVariable Long id) {
    return Response.success(postService.read(id));
}

게시글 id로 조회된 결과를 응답해줍니다.

 

게시글 조회는 모든 사용자가 요청할 수 있어야하고, /api/** 요청에 대해서는 permitAll로 설정해두었으므로, 이번에는 별다른 security 설정을 하지 않아도 되겠습니다.

 

 

PostController에 새롭게 작성된 read에 대해서 테스트해주겠습니다.

PostControllerTest, PostControllerAdviceTest, PostControllerIntegrationTest에 각각 다음과 같은 테스트를 추가해줍니다.

// PostControllerTest.java
@Test
void readTest() throws Exception {
    // given
    Long id = 1L;

    // when, then
    mockMvc.perform(
            get("/api/posts/{id}", id))
            .andExpect(status().isOk());
    verify(postService).read(id);
}
// PostControllerAdviceTest.java
@Test
void readExceptionByPostNotFoundTest() throws Exception {
    // given
    given(postService.read(anyLong())).willThrow(PostNotFoundException.class);

    // when, then
    mockMvc.perform(
            get("/api/posts/{id}", 1L))
            .andExpect(status().isNotFound())
            .andExpect(jsonPath("$.code").value(-1012));
}
// PostControllerIntegrationTest.java
@Test
void readTest() throws Exception {
    // given
    Post post = postRepository.save(createPost(member, category));

    // when, then
    mockMvc.perform(
            get("/api/posts/{id}", post.getId()))
            .andExpect(status().isOk());
}

자세한 설명은 생략하겠습니다.

 

테스트 성공

모든 테스트가 정상적으로 수행되었습니다.

 

 

포스트맨을 이용하여 게시글을 직접 작성하고, 작성된 게시글을 조회해보겠습니다.

게시글 생성 요청

위와 같이 두 개의 이미지를 첨부하여 게시글을 생성하였고, 생성된 게시글의 id를 응답받았습니다.

 

 

2번 게시글을 조회해보겠습니다.

GET /api/posts/2
{
    "success": true,
    "code": 0,
    "result": {
        "data": {
            "id": 2,
            "title": "글제목 입니다 ~~",
            "content": "글 내용입니다  ~~",
            "price": 1500,
            "member": {
                "id": 1,
                "email": "admin@admin.com",
                "username": "admin",
                "nickname": "admin"
            },
            "images": [
                {
                    "id": 1,
                    "originName": "1.PNG",
                    "uniqueName": "5d0b1b63-f4e2-481c-b81c-28d079cbffff.PNG"
                },
                {
                    "id": 2,
                    "originName": "2.PNG",
                    "uniqueName": "91e2c8fe-cfea-42dd-9200-e71626f7086f.PNG"
                }
            ]
        }
    }
}

2번 게시글에 대한 정보를 응답 받았습니다. 작성자의 정보도 확인할 수 있습니다.

 

 

이미지의 uniqueName으로 이미지에 접근해보겠습니다.

이미지 조회 성공

응답받은 이미지 경로에 대해 정상적인 접근도 확인할 수 있었습니다.

 

 

* 누락됐었던 내용 추가입니다.

dto.post.PostDto가 게시글 작성일자와 수정일자도 포함될 수 있도록 코드를 수정해줍니다.

@Data
@AllArgsConstructor
public class PostDto {
    private Long id;
    private String title;
    private String content;
    private Long price;
    private MemberDto member;
    private List<ImageDto> images;

    @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm:ss", timezone = "Asia/Seoul")
    private LocalDateTime createdAt;
    @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm:ss", timezone = "Asia/Seoul")
    private LocalDateTime modifiedAt;

    ...
}

응답 바디로 전송하기 위해 JSON으로 변환될 때, @JsonFormat으로 지정해둔 형태로 LocalDateTime이 변환되어 응답됩니다.

 

 

 

이번 시간에는 게시글 조회 API를 작성해보았습니다.

다음 시간에는 게시글 삭제 API를 작성해보겠습니다. 

 

 

* 질문 및 피드백은 환영입니다.

* 전체 소스코드에서는 여기에서 확인해볼 수 있습니다.

https://github.com/SongHeeJae/kuke-market

반응형

+ Recent posts