diff --git a/src/main/java/eatda/controller/article/ArticleController.java b/src/main/java/eatda/controller/article/ArticleController.java new file mode 100644 index 00000000..478f0a04 --- /dev/null +++ b/src/main/java/eatda/controller/article/ArticleController.java @@ -0,0 +1,24 @@ +package eatda.controller.article; + +import eatda.service.article.ArticleService; +import jakarta.validation.constraints.Max; +import jakarta.validation.constraints.Min; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestParam; + +@Controller +@RequiredArgsConstructor +public class ArticleController { + + private final ArticleService articleService; + + @GetMapping("/api/articles") + public ResponseEntity getArticles(@RequestParam(defaultValue = "3") @Min(1) @Max(50) int size) { + return ResponseEntity.status(HttpStatus.OK) + .body(articleService.getAllArticles(size)); + } +} diff --git a/src/main/java/eatda/controller/article/ArticleResponse.java b/src/main/java/eatda/controller/article/ArticleResponse.java new file mode 100644 index 00000000..9a998fdf --- /dev/null +++ b/src/main/java/eatda/controller/article/ArticleResponse.java @@ -0,0 +1,9 @@ +package eatda.controller.article; + +public record ArticleResponse( + String title, + String subtitle, + String articleUrl, + String imageUrl +) { +} diff --git a/src/main/java/eatda/controller/article/ArticlesResponse.java b/src/main/java/eatda/controller/article/ArticlesResponse.java new file mode 100644 index 00000000..d1b9d14c --- /dev/null +++ b/src/main/java/eatda/controller/article/ArticlesResponse.java @@ -0,0 +1,8 @@ +package eatda.controller.article; + +import java.util.List; + +public record ArticlesResponse( + List articles +) { +} diff --git a/src/main/java/eatda/repository/article/ArticleRepository.java b/src/main/java/eatda/repository/article/ArticleRepository.java new file mode 100644 index 00000000..95729476 --- /dev/null +++ b/src/main/java/eatda/repository/article/ArticleRepository.java @@ -0,0 +1,12 @@ +package eatda.repository.article; + +import eatda.domain.article.Article; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface ArticleRepository extends JpaRepository { + + Page
findAllByOrderByCreatedAtDesc(Pageable pageable); + +} diff --git a/src/main/java/eatda/service/article/ArticleService.java b/src/main/java/eatda/service/article/ArticleService.java new file mode 100644 index 00000000..29f117af --- /dev/null +++ b/src/main/java/eatda/service/article/ArticleService.java @@ -0,0 +1,33 @@ +package eatda.service.article; + +import eatda.controller.article.ArticleResponse; +import eatda.controller.article.ArticlesResponse; +import eatda.repository.article.ArticleRepository; +import eatda.service.common.ImageService; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.PageRequest; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class ArticleService { + + private final ArticleRepository articleRepository; + private final ImageService imageService; + + public ArticlesResponse getAllArticles(int size) { + PageRequest pageRequest = PageRequest.of(0, size); + List articles = articleRepository.findAllByOrderByCreatedAtDesc(pageRequest) + .stream() + .map(article -> new ArticleResponse( + article.getTitle(), + article.getSubtitle(), + article.getArticleUrl(), + imageService.getPresignedUrl(article.getImageKey()) + )) + .toList(); + + return new ArticlesResponse(articles); + } +} diff --git a/src/test/java/eatda/controller/BaseControllerTest.java b/src/test/java/eatda/controller/BaseControllerTest.java index 3827893d..3e16acfb 100644 --- a/src/test/java/eatda/controller/BaseControllerTest.java +++ b/src/test/java/eatda/controller/BaseControllerTest.java @@ -12,6 +12,7 @@ import eatda.client.oauth.OauthToken; import eatda.controller.web.jwt.JwtManager; import eatda.domain.member.Member; +import eatda.fixture.ArticleGenerator; import eatda.fixture.CheerGenerator; import eatda.fixture.MemberGenerator; import eatda.fixture.StoreGenerator; @@ -19,7 +20,6 @@ import eatda.repository.store.CheerRepository; import eatda.repository.store.StoreRepository; import eatda.service.common.ImageService; -import eatda.service.common.ImageService; import eatda.service.story.StoryService; import io.restassured.RestAssured; import io.restassured.builder.RequestSpecBuilder; @@ -59,6 +59,9 @@ public class BaseControllerTest { @Autowired protected CheerGenerator cheerGenerator; + @Autowired + protected ArticleGenerator articleGenerator; + @Autowired protected MemberRepository memberRepository; diff --git a/src/test/java/eatda/controller/article/ArticleControllerTest.java b/src/test/java/eatda/controller/article/ArticleControllerTest.java new file mode 100644 index 00000000..1224e866 --- /dev/null +++ b/src/test/java/eatda/controller/article/ArticleControllerTest.java @@ -0,0 +1,31 @@ +package eatda.controller.article; + +import static org.assertj.core.api.Assertions.assertThat; + +import eatda.controller.BaseControllerTest; +import eatda.domain.article.Article; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +public class ArticleControllerTest extends BaseControllerTest { + + @Nested + class GetArticles { + + @Test + void 가게의_담긴_이야기_목록을_조회할_수_있다() { + Article article1 = articleGenerator.generate("국밥의 모든 것"); + Article article2 = articleGenerator.generate("순대국의 진실"); + + ArticlesResponse response = given() + .queryParam("size", 3) + .when() + .get("/api/articles") + .then().statusCode(200) + .extract().as(ArticlesResponse.class); + + assertThat(response.articles()).hasSize(2); + assertThat(response.articles().getFirst().title()).isEqualTo("순대국의 진실"); + } + } +} diff --git a/src/test/java/eatda/document/BaseDocumentTest.java b/src/test/java/eatda/document/BaseDocumentTest.java index 3bb276a1..c35b4114 100644 --- a/src/test/java/eatda/document/BaseDocumentTest.java +++ b/src/test/java/eatda/document/BaseDocumentTest.java @@ -7,6 +7,7 @@ import eatda.controller.web.jwt.JwtManager; import eatda.exception.BusinessErrorCode; import eatda.exception.EtcErrorCode; +import eatda.service.article.ArticleService; import eatda.service.auth.AuthService; import eatda.service.common.ImageService; import eatda.service.member.MemberService; @@ -57,6 +58,10 @@ public abstract class BaseDocumentTest { @MockitoBean protected CheerService cheerService; + + @MockitoBean + protected ArticleService articleService; + @MockitoBean protected JwtManager jwtManager; diff --git a/src/test/java/eatda/document/Tag.java b/src/test/java/eatda/document/Tag.java index 61a9c130..dbce12b5 100644 --- a/src/test/java/eatda/document/Tag.java +++ b/src/test/java/eatda/document/Tag.java @@ -7,6 +7,7 @@ public enum Tag { STORE_API("Store API"), STORY_API("Story API"), CHEER_API("Cheer API"), + ARTICLE_API("Article API"), ; private final String displayName; diff --git a/src/test/java/eatda/document/article/ArticleDocumentTest.java b/src/test/java/eatda/document/article/ArticleDocumentTest.java new file mode 100644 index 00000000..510a1fe7 --- /dev/null +++ b/src/test/java/eatda/document/article/ArticleDocumentTest.java @@ -0,0 +1,74 @@ +package eatda.document.article; + +import static org.mockito.Mockito.doReturn; +import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; +import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName; + +import eatda.controller.article.ArticleResponse; +import eatda.controller.article.ArticlesResponse; +import eatda.document.BaseDocumentTest; +import eatda.document.RestDocsRequest; +import eatda.document.RestDocsResponse; +import eatda.document.Tag; +import io.restassured.response.Response; +import java.util.List; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.restdocs.restassured.RestDocumentationFilter; + +public class ArticleDocumentTest extends BaseDocumentTest { + + @Nested + class GetArticles { + + RestDocsRequest requestDocument = request() + .tag(Tag.ARTICLE_API) + .summary("가게의 담긴 이야기") + .description("게시글을 최신순으로 페이지네이션하여 조회합니다.") + .queryParameter(parameterWithName("size").description("페이지당 조회할 아티클 개수 (default = 3)")); + + RestDocsResponse responseDocument = response() + .responseBodyField( + fieldWithPath("articles").description("게시글 응답 리스트"), + fieldWithPath("articles[].title").description("게시글 제목"), + fieldWithPath("articles[].subtitle").description("게시글 소제목"), + fieldWithPath("articles[].articleUrl").description("게시글 링크 URL"), + fieldWithPath("articles[].imageUrl").description("게시글 이미지 URL") + ); + + @Test + void 가게의_담긴_이야기_목록_조회_성공() { + ArticlesResponse mockResponse = new ArticlesResponse(List.of( + new ArticleResponse( + "국밥의 모든 것", + "뜨끈한 국물의 세계", + "https://eatda.com/article/1", + "https://s3.bucket.com/article/1.jpg" + ), + new ArticleResponse( + "순대국의 진실", + "돼지부속의 미학", + "https://eatda.com/article/2", + "https://s3.bucket.com/article/2.jpg" + ) + )); + + doReturn(mockResponse) + .when(articleService) + .getAllArticles(3); + + RestDocumentationFilter document = document("article/get-articles", 200) + .request(requestDocument) + .response(responseDocument) + .build(); + + Response response = given(document) + .queryParam("size", 3) + .when() + .get("/api/articles"); + + response.then() + .statusCode(200); + } + } +} diff --git a/src/test/java/eatda/fixture/ArticleGenerator.java b/src/test/java/eatda/fixture/ArticleGenerator.java new file mode 100644 index 00000000..538133f7 --- /dev/null +++ b/src/test/java/eatda/fixture/ArticleGenerator.java @@ -0,0 +1,51 @@ +package eatda.fixture; + +import eatda.controller.article.ArticleResponse; +import eatda.domain.article.Article; +import eatda.repository.article.ArticleRepository; +import org.springframework.stereotype.Component; + +@Component +public class ArticleGenerator { + + private static final String DEFAULT_TITLE = "기본 제목"; + private static final String DEFAULT_SUBTITLE = "기본 소제목"; + private static final String DEFAULT_URL = "https://eatda.com/article/default"; + private static final String DEFAULT_IMAGE_KEY = "article/default-image.jpg"; + + private final ArticleRepository articleRepository; + + public ArticleGenerator(ArticleRepository articleRepository) { + this.articleRepository = articleRepository; + } + + public Article generate() { + return generate(DEFAULT_TITLE); + } + + public Article generate(String title) { + return generate(title, DEFAULT_SUBTITLE); + } + + public Article generate(String title, String subtitle) { + return generate(title, subtitle, DEFAULT_URL); + } + + public Article generate(String title, String subtitle, String articleUrl) { + return generate(title, subtitle, articleUrl, DEFAULT_IMAGE_KEY); + } + + public Article generate(String title, String subtitle, String articleUrl, String imageKey) { + Article article = new Article(title, subtitle, articleUrl, imageKey); + return articleRepository.save(article); + } + + public ArticleResponse toResponse(Article article) { + return new ArticleResponse( + article.getTitle(), + article.getSubtitle(), + article.getArticleUrl(), + "https://s3.bucket.com/" + article.getImageKey() + ); + } +} diff --git a/src/test/java/eatda/service/BaseServiceTest.java b/src/test/java/eatda/service/BaseServiceTest.java index 631d25a9..9d0f2777 100644 --- a/src/test/java/eatda/service/BaseServiceTest.java +++ b/src/test/java/eatda/service/BaseServiceTest.java @@ -7,9 +7,11 @@ import eatda.DatabaseCleaner; import eatda.client.map.MapClient; import eatda.client.oauth.OauthClient; +import eatda.fixture.ArticleGenerator; import eatda.fixture.CheerGenerator; import eatda.fixture.MemberGenerator; import eatda.fixture.StoreGenerator; +import eatda.repository.article.ArticleRepository; import eatda.repository.member.MemberRepository; import eatda.repository.store.CheerRepository; import eatda.repository.store.StoreRepository; @@ -45,6 +47,9 @@ public abstract class BaseServiceTest { @Autowired protected CheerGenerator cheerGenerator; + @Autowired + protected ArticleGenerator articleGenerator; + @Autowired protected MemberRepository memberRepository; @@ -54,6 +59,9 @@ public abstract class BaseServiceTest { @Autowired protected CheerRepository cheerRepository; + @Autowired + protected ArticleRepository articleRepository; + @BeforeEach void mockingImageService() { doReturn(MOCKED_IMAGE_URL).when(imageService).getPresignedUrl(anyString()); diff --git a/src/test/java/eatda/service/article/ArticleServiceTest.java b/src/test/java/eatda/service/article/ArticleServiceTest.java new file mode 100644 index 00000000..b138cc81 --- /dev/null +++ b/src/test/java/eatda/service/article/ArticleServiceTest.java @@ -0,0 +1,32 @@ +package eatda.service.article; + +import static org.assertj.core.api.Assertions.assertThat; + +import eatda.controller.article.ArticleResponse; +import eatda.service.BaseServiceTest; +import java.util.stream.LongStream; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; + +public class ArticleServiceTest extends BaseServiceTest { + + @Autowired + private ArticleService articleService; + + @Nested + class GetAllArticles { + + @Test + void 가게의_담긴_이야기를_최신순으로_조회할_수_있다() { + LongStream.rangeClosed(1, 5) + .forEach(i -> articleGenerator.generate("아티클 제목 " + i)); + + var response = articleService.getAllArticles(3); + + assertThat(response.articles()).hasSize(3) + .extracting(ArticleResponse::title) + .containsExactly("아티클 제목 5", "아티클 제목 4", "아티클 제목 3"); + } + } +}