diff --git a/src/main/java/com/yam/app/article/application/ArticleFacade.java b/src/main/java/com/yam/app/article/application/ArticleFacade.java index 8416e32..75bb6b3 100644 --- a/src/main/java/com/yam/app/article/application/ArticleFacade.java +++ b/src/main/java/com/yam/app/article/application/ArticleFacade.java @@ -1,8 +1,11 @@ package com.yam.app.article.application; +import com.yam.app.article.domain.ArticleNotFoundException; import com.yam.app.article.domain.ArticleReader; import com.yam.app.article.domain.WriteArticleProcessor; import com.yam.app.article.presentation.ArticlePreviewResponse; +import com.yam.app.article.presentation.ArticleResponse; +import com.yam.app.article.presentation.TagResponse; import com.yam.app.article.presentation.WriteArticleCommand; import java.util.List; import java.util.stream.Collectors; @@ -38,4 +41,26 @@ public class ArticleFacade { dto.getStatus()) ).collect(Collectors.toList()); } + + @Transactional(readOnly = true) + public ArticleResponse findById(Long articleId) { + var article = articleReader.findById(articleId).orElseThrow( + () -> new ArticleNotFoundException(articleId)); + + return ArticleResponse.builder() + .id(article.getId()) + .authorId(article.getAuthorId()) + .title(article.getTitle()) + .content(article.getContent()) + .image(article.getImage()) + .createdAt(article.getCreatedAt()) + .modifiedAt(article.getModifiedAt()) + .tags( + article.getTags().stream() + .map(a -> TagResponse.of(a.getTag().getId(), a.getTag().getName())) + .collect(Collectors.toList()) + ) + .build(); + + } } diff --git a/src/main/java/com/yam/app/article/presentation/ArticleQueryApi.java b/src/main/java/com/yam/app/article/presentation/ArticleQueryApi.java index 789730a..9e06114 100644 --- a/src/main/java/com/yam/app/article/presentation/ArticleQueryApi.java +++ b/src/main/java/com/yam/app/article/presentation/ArticleQueryApi.java @@ -5,6 +5,7 @@ import com.yam.app.common.ApiResult; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; @@ -29,4 +30,9 @@ public final class ArticleQueryApi { return ResponseEntity.ok( ApiResult.success(articleFacade.findAll(offset, limit))); } + + @GetMapping("/api/articles/{articleId}") + public ResponseEntity showArticle(@PathVariable Long articleId) { + return ResponseEntity.ok(articleFacade.findById(articleId)); + } } diff --git a/src/main/java/com/yam/app/article/presentation/ArticleResponse.java b/src/main/java/com/yam/app/article/presentation/ArticleResponse.java new file mode 100644 index 0000000..d45ebea --- /dev/null +++ b/src/main/java/com/yam/app/article/presentation/ArticleResponse.java @@ -0,0 +1,34 @@ +package com.yam.app.article.presentation; + +import java.time.LocalDateTime; +import java.util.List; +import lombok.Builder; +import lombok.Getter; + +@Getter +@Builder +public final class ArticleResponse { + + private final Long id; + private final Long authorId; + private final String title; + private final String content; + private final String image; + private final LocalDateTime createdAt; + private final LocalDateTime modifiedAt; + private final List tags; + + private ArticleResponse(Long id, Long authorId, String title, String content, String image, + LocalDateTime createdAt, LocalDateTime modifiedAt, + List tags) { + this.id = id; + this.authorId = authorId; + this.title = title; + this.content = content; + this.image = image; + this.createdAt = createdAt; + this.modifiedAt = modifiedAt; + this.tags = tags; + } + +} diff --git a/src/main/java/com/yam/app/article/presentation/TagResponse.java b/src/main/java/com/yam/app/article/presentation/TagResponse.java new file mode 100644 index 0000000..d4dee1c --- /dev/null +++ b/src/main/java/com/yam/app/article/presentation/TagResponse.java @@ -0,0 +1,20 @@ +package com.yam.app.article.presentation; + +import lombok.Getter; + +@Getter +public final class TagResponse { + + private final Long id; + private final String name; + + private TagResponse(Long id, String name) { + this.id = id; + this.name = name; + } + + public static TagResponse of(Long id, String name) { + return new TagResponse(id, name); + } + +} diff --git a/src/test/java/com/yam/app/article/presentation/ArticleApiUri.java b/src/test/java/com/yam/app/article/presentation/ArticleApiUri.java index 5082996..890588d 100644 --- a/src/test/java/com/yam/app/article/presentation/ArticleApiUri.java +++ b/src/test/java/com/yam/app/article/presentation/ArticleApiUri.java @@ -4,6 +4,8 @@ public final class ArticleApiUri { public static final String WRITE_ARTICLE = "/api/articles/write"; public static final String FIND_ALL = "/api/articles/all"; + public static final String FIND_BY_ID = "/api/articles/"; - private ArticleApiUri() {} + private ArticleApiUri() { + } } diff --git a/src/test/java/com/yam/app/article/presentation/ArticleQueryApiTest.java b/src/test/java/com/yam/app/article/presentation/ArticleQueryApiTest.java new file mode 100644 index 0000000..3d5a711 --- /dev/null +++ b/src/test/java/com/yam/app/article/presentation/ArticleQueryApiTest.java @@ -0,0 +1,45 @@ +package com.yam.app.article.presentation; + +import static com.yam.app.article.presentation.ArticleApiUri.FIND_BY_ID; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import com.yam.app.article.application.ArticleFacade; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.http.MediaType; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.web.servlet.MockMvc; + +@DisplayName("Article Qurey HTTP API") +@WebMvcTest(ArticleQueryApi.class) +@ActiveProfiles("test") +class ArticleQueryApiTest { + + @Autowired + private MockMvc mockMvc; + @MockBean + private ArticleFacade articleFacade; + + @Test + @DisplayName("인증되지 않은 사용자가 게시글 조회 요청을 보냈다면 401에러를 반환한다.") + void unauthenticated_user_request() throws Exception { + //Act + final var actions = mockMvc.perform(get(FIND_BY_ID + 1) + .contentType(MediaType.APPLICATION_JSON) + .accept(MediaType.APPLICATION_JSON) + ); + + //Assert + actions + .andExpect(status().isUnauthorized()) + .andExpect(jsonPath("$.success").value(false)) + .andExpect(jsonPath("$.data").doesNotExist()) + .andExpect(jsonPath("$.message").value("Unauthorized request")); + + } +}