Implements article pagination query HTTP API

This commit is contained in:
Rebwon
2021-10-18 12:55:39 +09:00
committed by MaengSol
parent f15ea65c06
commit 58c7e8922a
13 changed files with 206 additions and 16 deletions

View File

@@ -12,7 +12,8 @@ public class WebConfiguration implements WebMvcConfigurer {
private static final String[] EXCLUDE_PATHS = { private static final String[] EXCLUDE_PATHS = {
"/api/accounts/login", "/api/accounts/login",
"/api/accounts/authorize", "/api/accounts/authorize",
"/api/accounts" "/api/accounts",
"/api/articles/all"
}; };
@Override @Override

View File

@@ -1,7 +1,11 @@
package com.yam.app.article.application; 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.domain.WriteArticleProcessor;
import com.yam.app.article.presentation.ArticlePreviewResponse;
import com.yam.app.article.presentation.WriteArticleCommand; import com.yam.app.article.presentation.WriteArticleCommand;
import java.util.List;
import java.util.stream.Collectors;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
@@ -9,9 +13,12 @@ import org.springframework.transaction.annotation.Transactional;
public class ArticleFacade { public class ArticleFacade {
private final WriteArticleProcessor writeArticleProcessor; private final WriteArticleProcessor writeArticleProcessor;
private final ArticleReader articleReader;
public ArticleFacade(WriteArticleProcessor writeArticleProcessor) { public ArticleFacade(WriteArticleProcessor writeArticleProcessor,
ArticleReader articleReader) {
this.writeArticleProcessor = writeArticleProcessor; this.writeArticleProcessor = writeArticleProcessor;
this.articleReader = articleReader;
} }
@Transactional @Transactional
@@ -20,4 +27,15 @@ public class ArticleFacade {
command.getTitle(), command.getContent(), command.getTitle(), command.getContent(),
command.getImage(), command.getTags()); 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());
}
} }

View 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;
}

View File

@@ -12,5 +12,8 @@ public interface ArticleReader {
boolean existsById(Long articleId); 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);
} }

View File

@@ -1,6 +1,7 @@
package com.yam.app.article.infrastructure; package com.yam.app.article.infrastructure;
import com.yam.app.article.domain.Article; 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.ArticleReader;
import com.yam.app.article.domain.ArticleRepository; import com.yam.app.article.domain.ArticleRepository;
import java.util.List; import java.util.List;
@@ -42,8 +43,13 @@ public final class MybatisArticleRepository implements ArticleReader, ArticleRep
} }
@Override @Override
public List<Long> findAll(int offset, int limit) { public List<Long> findAll() {
return template.getMapper(ArticleReader.class).findAll(offset, limit); 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);
} }
} }

View File

@@ -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;
}
}

View File

@@ -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)));
}
}

View File

@@ -7,16 +7,43 @@
SELECT SELECT
DISTINCT(a.id) AS article_id, a.created_at DISTINCT(a.id) AS article_id, a.created_at
FROM article a FROM article a
INNER JOIN article_tag atg ON atg.article_id = a.id LEFT OUTER JOIN article_tag atg ON atg.article_id = a.id
INNER JOIN tag t ON t.id = atg.tag_id LEFT OUTER JOIN tag t ON t.id = atg.tag_id
ORDER BY a.created_at DESC
LIMIT #{offset}, #{limit}
</select> </select>
<resultMap id="articleId" type="Long"> <resultMap id="articleId" type="Long">
<id javaType="Long" column="article_id"/> <id javaType="Long" column="article_id"/>
</resultMap> </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 id="existsById" parameterType="Long" resultType="boolean">
SELECT COUNT(*) SELECT COUNT(*)
FROM ARTICLE FROM ARTICLE

View File

@@ -38,7 +38,12 @@ public final class FakeArticleRepository implements ArticleRepository, ArticleRe
} }
@Override @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(); throw new UnsupportedOperationException();
} }
} }

View File

@@ -1,7 +1,11 @@
package com.yam.app.article.infrastructure; 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 com.yam.app.article.domain.ArticleReader;
import java.util.List; import java.util.List;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.context.SpringBootTest;
@@ -9,13 +13,17 @@ import org.springframework.test.context.ActiveProfiles;
@SpringBootTest @SpringBootTest
@ActiveProfiles("test") @ActiveProfiles("test")
@Disabled
public final class ArticleReaderTest { public final class ArticleReaderTest {
@Autowired @Autowired
ArticleReader articleReader; ArticleReader articleReader;
@Test @Test
void name() { void articlePaginationQueryTests() {
List<Long> idx = articleReader.findAll(0, 10); List<Long> idx = articleReader.findAll();
List<ArticleDto> dtos = articleReader.findAllById(1, 10, idx);
assertThat(dtos.size()).isEqualTo(10);
} }
} }

View File

@@ -3,6 +3,7 @@ package com.yam.app.article.presentation;
public final class ArticleApiUri { public final class ArticleApiUri {
public static final String WRITE_ARTICLE = "/api/articles/write"; public static final String WRITE_ARTICLE = "/api/articles/write";
public static final String FIND_ALL = "/api/articles/all";
private ArticleApiUri() {} private ArticleApiUri() {}
} }

View File

@@ -1,8 +1,12 @@
package com.yam.app.integration; package com.yam.app.integration;
import static com.yam.app.account.presentation.AccountApiUri.LOGIN; 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 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.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 static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import com.yam.app.account.presentation.LoginAccountCommand; import com.yam.app.account.presentation.LoginAccountCommand;
@@ -10,6 +14,7 @@ import com.yam.app.article.presentation.WriteArticleCommand;
import java.util.List; import java.util.List;
import org.javaunit.autoparams.AutoSource; import org.javaunit.autoparams.AutoSource;
import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.ParameterizedTest;
import org.springframework.http.MediaType; import org.springframework.http.MediaType;
@@ -52,4 +57,17 @@ final class ArticleIntegrationTests extends AbstractIntegrationTests {
.andExpect(status().isCreated()); .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());
}
} }

View File

@@ -21,7 +21,31 @@ values ('rebwon@gmail.com', 'emailchecktoken1', now(), true, now(), now(),
false, 'DEFAULT', 3, 'ALIVE'); false, 'DEFAULT', 3, 'ALIVE');
insert into article(title, content, image, status, created_at, modified_at, member_id) 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) insert into tag(name)
values ('hibernate'), values ('hibernate'),
@@ -29,9 +53,9 @@ values ('hibernate'),
('jpa'); ('jpa');
insert into article_tag(article_id, tag_id) insert into article_tag(article_id, tag_id)
values (1, 1), 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),
(1, 2), (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),
(1, 3); (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) INSERT INTO comment(content, created_at, modified_at, status, article_id, member_id)
VALUES ('sample content1', now(), now(), 'ALIVE', 1, 3); VALUES ('sample content1', now(), now(), 'ALIVE', 1, 3);