Implements article pagination query HTTP API
This commit is contained in:
@@ -12,7 +12,8 @@ public class WebConfiguration implements WebMvcConfigurer {
|
||||
private static final String[] EXCLUDE_PATHS = {
|
||||
"/api/accounts/login",
|
||||
"/api/accounts/authorize",
|
||||
"/api/accounts"
|
||||
"/api/accounts",
|
||||
"/api/articles/all"
|
||||
};
|
||||
|
||||
@Override
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
package com.yam.app.article.application;
|
||||
|
||||
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.WriteArticleCommand;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
@@ -9,9 +13,12 @@ import org.springframework.transaction.annotation.Transactional;
|
||||
public class ArticleFacade {
|
||||
|
||||
private final WriteArticleProcessor writeArticleProcessor;
|
||||
private final ArticleReader articleReader;
|
||||
|
||||
public ArticleFacade(WriteArticleProcessor writeArticleProcessor) {
|
||||
public ArticleFacade(WriteArticleProcessor writeArticleProcessor,
|
||||
ArticleReader articleReader) {
|
||||
this.writeArticleProcessor = writeArticleProcessor;
|
||||
this.articleReader = articleReader;
|
||||
}
|
||||
|
||||
@Transactional
|
||||
@@ -20,4 +27,15 @@ public class ArticleFacade {
|
||||
command.getTitle(), command.getContent(),
|
||||
command.getImage(), command.getTags());
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public List<ArticlePreviewResponse> findAll(int offset, int limit) {
|
||||
var idx = articleReader.findAll();
|
||||
return articleReader.findAllById(offset, limit, idx)
|
||||
.stream()
|
||||
.map(dto -> new ArticlePreviewResponse(dto.getId(), dto.getAuthorId(), dto.getTitle(),
|
||||
dto.getNickname(), dto.getImage(), dto.getCreatedAt(), dto.getModifiedAt(),
|
||||
dto.getStatus())
|
||||
).collect(Collectors.toList());
|
||||
}
|
||||
}
|
||||
|
||||
17
src/main/java/com/yam/app/article/domain/ArticleDto.java
Normal file
17
src/main/java/com/yam/app/article/domain/ArticleDto.java
Normal file
@@ -0,0 +1,17 @@
|
||||
package com.yam.app.article.domain;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public final class ArticleDto {
|
||||
|
||||
private Long id;
|
||||
private Long authorId;
|
||||
private String title;
|
||||
private String status;
|
||||
private LocalDateTime createdAt;
|
||||
private LocalDateTime modifiedAt;
|
||||
private String nickname;
|
||||
private String image;
|
||||
}
|
||||
@@ -12,5 +12,8 @@ public interface ArticleReader {
|
||||
|
||||
boolean existsById(Long articleId);
|
||||
|
||||
List<Long> findAll(@Param("offset") int offset, @Param("limit") int limit);
|
||||
List<Long> findAll();
|
||||
|
||||
List<ArticleDto> findAllById(@Param("offset") int offset,
|
||||
@Param("limit") int limit, @Param("idx") List<Long> idx);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.yam.app.article.infrastructure;
|
||||
|
||||
import com.yam.app.article.domain.Article;
|
||||
import com.yam.app.article.domain.ArticleDto;
|
||||
import com.yam.app.article.domain.ArticleReader;
|
||||
import com.yam.app.article.domain.ArticleRepository;
|
||||
import java.util.List;
|
||||
@@ -42,8 +43,13 @@ public final class MybatisArticleRepository implements ArticleReader, ArticleRep
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Long> findAll(int offset, int limit) {
|
||||
return template.getMapper(ArticleReader.class).findAll(offset, limit);
|
||||
public List<Long> findAll() {
|
||||
return template.getMapper(ArticleReader.class).findAll();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ArticleDto> findAllById(int offset, int limit, List<Long> idx) {
|
||||
return template.getMapper(ArticleReader.class).findAllById(offset, limit, idx);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
package com.yam.app.article.presentation;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import lombok.Getter;
|
||||
|
||||
@Getter
|
||||
public final class ArticlePreviewResponse {
|
||||
|
||||
private final Long id;
|
||||
private final Long authorId;
|
||||
private final String title;
|
||||
private final String nickname;
|
||||
private final String memberImage;
|
||||
private final LocalDateTime createdAt;
|
||||
private final LocalDateTime modifiedAt;
|
||||
private final String status;
|
||||
|
||||
public ArticlePreviewResponse(Long id, Long authorId, String title,
|
||||
String nickname, String memberImage, LocalDateTime createdAt,
|
||||
LocalDateTime modifiedAt, String status) {
|
||||
this.id = id;
|
||||
this.authorId = authorId;
|
||||
this.title = title;
|
||||
this.nickname = nickname;
|
||||
this.memberImage = memberImage;
|
||||
this.createdAt = createdAt;
|
||||
this.modifiedAt = modifiedAt;
|
||||
this.status = status;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package com.yam.app.article.presentation;
|
||||
|
||||
import com.yam.app.article.application.ArticleFacade;
|
||||
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.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
@RestController
|
||||
@RequestMapping(
|
||||
produces = MediaType.APPLICATION_JSON_VALUE,
|
||||
consumes = MediaType.APPLICATION_JSON_VALUE
|
||||
)
|
||||
public final class ArticleQueryApi {
|
||||
|
||||
private final ArticleFacade articleFacade;
|
||||
|
||||
public ArticleQueryApi(ArticleFacade articleFacade) {
|
||||
this.articleFacade = articleFacade;
|
||||
}
|
||||
|
||||
@GetMapping("/api/articles/all")
|
||||
public ResponseEntity<?> findAll(
|
||||
@RequestParam(value = "offset", defaultValue = "0") int offset,
|
||||
@RequestParam(value = "limit", defaultValue = "20") int limit) {
|
||||
return ResponseEntity.ok(
|
||||
ApiResult.success(articleFacade.findAll(offset, limit)));
|
||||
}
|
||||
}
|
||||
@@ -7,16 +7,43 @@
|
||||
SELECT
|
||||
DISTINCT(a.id) AS article_id, a.created_at
|
||||
FROM article a
|
||||
INNER JOIN article_tag atg ON atg.article_id = a.id
|
||||
INNER JOIN tag t ON t.id = atg.tag_id
|
||||
ORDER BY a.created_at DESC
|
||||
LIMIT #{offset}, #{limit}
|
||||
LEFT OUTER JOIN article_tag atg ON atg.article_id = a.id
|
||||
LEFT OUTER JOIN tag t ON t.id = atg.tag_id
|
||||
</select>
|
||||
|
||||
<resultMap id="articleId" type="Long">
|
||||
<id javaType="Long" column="article_id"/>
|
||||
</resultMap>
|
||||
|
||||
<select id="findAllById" resultMap="articleDto" >
|
||||
SELECT a.id AS article_id,
|
||||
a.title AS article_title,
|
||||
a.status AS article_status,
|
||||
a.member_id AS article_author_id,
|
||||
a.created_at AS article_created_at,
|
||||
a.modified_at AS article_modified_at,
|
||||
m.nickname AS member_nickname,
|
||||
m.image AS member_image
|
||||
FROM article a
|
||||
INNER JOIN member m ON m.id = a.member_id
|
||||
WHERE a.id IN
|
||||
<foreach index="index" collection="idx" item="id" open="(" separator="," close=")">
|
||||
#{id}
|
||||
</foreach>
|
||||
LIMIT #{offset}, #{limit}
|
||||
</select>
|
||||
|
||||
<resultMap id="articleDto" type="com.yam.app.article.domain.ArticleDto">
|
||||
<id property="id" column = "article_id"/>
|
||||
<result property="title" column="article_title"/>
|
||||
<result property="status" column="article_status"/>
|
||||
<result property="authorId" column = "article_author_id"/>
|
||||
<result property="createdAt" column="article_created_at"/>
|
||||
<result property="modifiedAt" column="article_modified_at"/>
|
||||
<result property="nickname" column="member_nickname"/>
|
||||
<result property="image" column="member_image"/>
|
||||
</resultMap>
|
||||
|
||||
<select id="existsById" parameterType="Long" resultType="boolean">
|
||||
SELECT COUNT(*)
|
||||
FROM ARTICLE
|
||||
|
||||
@@ -38,7 +38,12 @@ public final class FakeArticleRepository implements ArticleRepository, ArticleRe
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Long> findAll(int offset, int limit) {
|
||||
public List<Long> findAll() {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ArticleDto> findAllById(int offset, int limit, List<Long> idx) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
package com.yam.app.article.infrastructure;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
import com.yam.app.article.domain.ArticleDto;
|
||||
import com.yam.app.article.domain.ArticleReader;
|
||||
import java.util.List;
|
||||
import org.junit.jupiter.api.Disabled;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
@@ -9,13 +13,17 @@ import org.springframework.test.context.ActiveProfiles;
|
||||
|
||||
@SpringBootTest
|
||||
@ActiveProfiles("test")
|
||||
@Disabled
|
||||
public final class ArticleReaderTest {
|
||||
|
||||
@Autowired
|
||||
ArticleReader articleReader;
|
||||
|
||||
@Test
|
||||
void name() {
|
||||
List<Long> idx = articleReader.findAll(0, 10);
|
||||
void articlePaginationQueryTests() {
|
||||
List<Long> idx = articleReader.findAll();
|
||||
List<ArticleDto> dtos = articleReader.findAllById(1, 10, idx);
|
||||
|
||||
assertThat(dtos.size()).isEqualTo(10);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package com.yam.app.article.presentation;
|
||||
public final class ArticleApiUri {
|
||||
|
||||
public static final String WRITE_ARTICLE = "/api/articles/write";
|
||||
public static final String FIND_ALL = "/api/articles/all";
|
||||
|
||||
private ArticleApiUri() {}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
package com.yam.app.integration;
|
||||
|
||||
import static com.yam.app.account.presentation.AccountApiUri.LOGIN;
|
||||
import static com.yam.app.article.presentation.ArticleApiUri.FIND_ALL;
|
||||
import static com.yam.app.article.presentation.ArticleApiUri.WRITE_ARTICLE;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
||||
|
||||
import com.yam.app.account.presentation.LoginAccountCommand;
|
||||
@@ -10,6 +14,7 @@ import com.yam.app.article.presentation.WriteArticleCommand;
|
||||
import java.util.List;
|
||||
import org.javaunit.autoparams.AutoSource;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.springframework.http.MediaType;
|
||||
|
||||
@@ -52,4 +57,17 @@ final class ArticleIntegrationTests extends AbstractIntegrationTests {
|
||||
.andExpect(status().isCreated());
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("기본적으로 아무 페이징 조건을 주지 않을 경우, "
|
||||
+ "20건을 생성 날짜 내림차순으로 정렬하여 보여준다.")
|
||||
void default_main_page_find_all_preview_article_response() throws Exception {
|
||||
// Act
|
||||
mockMvc.perform(get(FIND_ALL)
|
||||
.accept(MediaType.APPLICATION_JSON)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
)
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.data").isArray());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,7 +21,31 @@ values ('rebwon@gmail.com', 'emailchecktoken1', now(), true, now(), now(),
|
||||
false, 'DEFAULT', 3, 'ALIVE');
|
||||
|
||||
insert into article(title, content, image, status, created_at, modified_at, member_id)
|
||||
values ('sample-title', 'sample-content', 'sample.png', 'ALIVE', now(), now(), 1);
|
||||
values ('sample-title', 'sample-content', 'sample.png', 'ALIVE', now(), now(), 1),
|
||||
('sample-title1', 'sample-content', 'sample.png', 'ALIVE', now(), now(), 2),
|
||||
('sample-title2', 'sample-content', 'sample.png', 'ALIVE', now(), now(), 3),
|
||||
('sample-title3', 'sample-content', 'sample.png', 'ALIVE', now(), now(), 3),
|
||||
('sample-title4', 'sample-content', 'sample.png', 'ALIVE', now(), now(), 2),
|
||||
('sample-title5', 'sample-content', 'sample.png', 'ALIVE', now(), now(), 1),
|
||||
('sample-title6', 'sample-content', 'sample.png', 'ALIVE', now(), now(), 2),
|
||||
('sample-title7', 'sample-content', 'sample.png', 'ALIVE', now(), now(), 3),
|
||||
('sample-title8', 'sample-content', 'sample.png', 'ALIVE', now(), now(), 2),
|
||||
('sample-title9', 'sample-content', 'sample.png', 'ALIVE', now(), now(), 1),
|
||||
('sample-title10', 'sample-content', 'sample.png', 'ALIVE', now(), now(), 2),
|
||||
('sample-title11', 'sample-content', 'sample.png', 'ALIVE', now(), now(), 3),
|
||||
('sample-title12', 'sample-content', 'sample.png', 'ALIVE', now(), now(), 2),
|
||||
('sample-title13', 'sample-content', 'sample.png', 'ALIVE', now(), now(), 1),
|
||||
('sample-title14', 'sample-content', 'sample.png', 'ALIVE', now(), now(), 2),
|
||||
('sample-title15', 'sample-content', 'sample.png', 'ALIVE', now(), now(), 3),
|
||||
('sample-title16', 'sample-content', 'sample.png', 'ALIVE', now(), now(), 2),
|
||||
('sample-title17', 'sample-content', 'sample.png', 'ALIVE', now(), now(), 1),
|
||||
('sample-title18', 'sample-content', 'sample.png', 'ALIVE', now(), now(), 2),
|
||||
('sample-title19', 'sample-content', 'sample.png', 'ALIVE', now(), now(), 3),
|
||||
('sample-title20', 'sample-content', 'sample.png', 'ALIVE', now(), now(), 2),
|
||||
('sample-title21', 'sample-content', 'sample.png', 'ALIVE', now(), now(), 1),
|
||||
('sample-title22', 'sample-content', 'sample.png', 'ALIVE', now(), now(), 2),
|
||||
('sample-title23', 'sample-content', 'sample.png', 'ALIVE', now(), now(), 3),
|
||||
('sample-title24', 'sample-content', 'sample.png', 'ALIVE', now(), now(), 1);
|
||||
|
||||
insert into tag(name)
|
||||
values ('hibernate'),
|
||||
@@ -29,9 +53,9 @@ values ('hibernate'),
|
||||
('jpa');
|
||||
|
||||
insert into article_tag(article_id, tag_id)
|
||||
values (1, 1),
|
||||
(1, 2),
|
||||
(1, 3);
|
||||
values (1, 1), (1, 2), (1, 3), (2, 2), (2, 3), (3, 1), (3, 2), (4, 1), (4, 2), (5, 1), (5, 2), (6, 2), (6, 3), (7, 1), (7, 2), (7, 3),
|
||||
(8, 1), (8, 2), (9, 1), (9, 2), (9, 3), (10, 1), (10, 2), (11, 1), (11, 2), (11, 3), (12, 1), (12, 3), (13, 1), (13, 2), (14, 1),
|
||||
(15, 2), (16, 1), (17, 1), (18, 2), (19, 1), (19, 2), (19, 3), (20, 1), (20, 2);
|
||||
|
||||
INSERT INTO comment(content, created_at, modified_at, status, article_id, member_id)
|
||||
VALUES ('sample content1', now(), now(), 'ALIVE', 1, 3);
|
||||
|
||||
Reference in New Issue
Block a user