From 203cc2a2ed273cb264d6bb7bfc48f40745b91798 Mon Sep 17 00:00:00 2001 From: splitCoding Date: Fri, 11 Aug 2023 16:33:21 +0900 Subject: [PATCH 01/71] =?UTF-8?q?refactor:=20=EC=95=84=ED=8B=B0=ED=81=B4?= =?UTF-8?q?=20=EC=83=9D=EC=84=B1=EC=8B=9C=20CREW=20=EA=B6=8C=ED=95=9C=20?= =?UTF-8?q?=ED=99=95=EC=9D=B8=20=EB=A1=9C=EC=A7=81=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../article/application/ArticleService.java | 3 +++ .../prolog/common/exception/BadRequestCode.java | 5 +++-- .../article/application/ArticleServiceTest.java | 15 +++++++++++++++ 3 files changed, 21 insertions(+), 2 deletions(-) diff --git a/backend/src/main/java/wooteco/prolog/article/application/ArticleService.java b/backend/src/main/java/wooteco/prolog/article/application/ArticleService.java index b3b2d3054..8e25d4ae2 100644 --- a/backend/src/main/java/wooteco/prolog/article/application/ArticleService.java +++ b/backend/src/main/java/wooteco/prolog/article/application/ArticleService.java @@ -27,6 +27,9 @@ public class ArticleService { @Transactional public Long create(final ArticleRequest articleRequest, final LoginMember loginMember) { final Member member = memberService.findById(loginMember.getId()); + if (member.isAnonymous()) { + throw new BadRequestException(BadRequestCode.UNVALIDATED_MEMBER_EXCEPTION); + } final Article article = articleRequest.toArticle(member); return articleRepository.save(article).getId(); } diff --git a/backend/src/main/java/wooteco/prolog/common/exception/BadRequestCode.java b/backend/src/main/java/wooteco/prolog/common/exception/BadRequestCode.java index e5ff32781..d263b0792 100644 --- a/backend/src/main/java/wooteco/prolog/common/exception/BadRequestCode.java +++ b/backend/src/main/java/wooteco/prolog/common/exception/BadRequestCode.java @@ -88,8 +88,9 @@ public enum BadRequestCode { INVALID_ARTICLE_AUTHOR_EXCEPTION(12005, "INVALID_ARTICLE_AUTHOR_EXCEPTION"), ARTICLE_IMAGE_URL_NULL_OR_EMPTY_EXCEPTION(12006, "ARTICLE_IMAGE_URL_NULL_OR_EMPTY_EXCEPTION"), ARTICLE_IMAGE_URL_OVER_LENGTH_EXCEPTION(12007, "ARTICLE_IMAGE_URL_OVER_LENGTH_EXCEPTION"), - ARTICLE_INVALID_URL_EXCEPTION(12008, "ARTICLE_INVALID_URL_EXCEPTION"); - + ARTICLE_INVALID_URL_EXCEPTION(12008, "ARTICLE_INVALID_URL_EXCEPTION"), + UNVALIDATED_MEMBER_EXCEPTION(12009, "UNVALIDATED_MEMBER_EXCEPTION"); + private int code; private String message; } diff --git a/backend/src/test/java/wooteco/prolog/article/application/ArticleServiceTest.java b/backend/src/test/java/wooteco/prolog/article/application/ArticleServiceTest.java index 9c67907f1..b324068f3 100644 --- a/backend/src/test/java/wooteco/prolog/article/application/ArticleServiceTest.java +++ b/backend/src/test/java/wooteco/prolog/article/application/ArticleServiceTest.java @@ -59,6 +59,21 @@ void create_success() { verify(articleRepository).save(any()); } + @DisplayName("아티클 생성시 UNVALIDATED 권한일 경우 예외를 발생한다.") + @Test + void create_success_unAuthorized() { + //given + final ArticleRequest judyRequest = new ArticleRequest("title", "url", "imageUrl"); + final Member member = new Member(1L, "username", "nickname", Role.UNVALIDATED, 1L, "url"); + final LoginMember judyLogin = new LoginMember(1L, MEMBER); + + when(memberService.findById(any())).thenReturn(member); + + //when + assertThatThrownBy(() -> articleService.create(judyRequest, judyLogin)) + .isInstanceOf(BadRequestException.class); + } + @DisplayName("아티클을 수정한다.") @Test void update_success() { From 047159ac6418892afe6a7d6148e8296545ecbc7f Mon Sep 17 00:00:00 2001 From: hong-sile Date: Wed, 13 Sep 2023 17:14:35 +0900 Subject: [PATCH 02/71] =?UTF-8?q?test:=20=EC=95=84=ED=8B=B0=ED=81=B4=20?= =?UTF-8?q?=EB=B6=81=EB=A7=88=ED=81=AC=20restdocs=20=ED=85=8C=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/src/documentation/adoc/article.adoc | 12 ++++++ .../prolog/docu/ArticleDocumentation.java | 40 +++++++++++++++++++ .../article/ui/ArticleBookmarkRequest.java | 15 +++++++ 3 files changed, 67 insertions(+) create mode 100644 backend/src/documentation/adoc/article.adoc create mode 100644 backend/src/documentation/java/wooteco/prolog/docu/ArticleDocumentation.java create mode 100644 backend/src/main/java/wooteco/prolog/article/ui/ArticleBookmarkRequest.java diff --git a/backend/src/documentation/adoc/article.adoc b/backend/src/documentation/adoc/article.adoc new file mode 100644 index 000000000..4c913ca77 --- /dev/null +++ b/backend/src/documentation/adoc/article.adoc @@ -0,0 +1,12 @@ +[[article]] +== 아티클 + +=== 아티클 북마크 추가 + +==== Request + +include::{snippets}/article/bookmark/http-request.adoc[] + +==== Response + +include::{snippets}/article/bookmark/http-response.adoc[] diff --git a/backend/src/documentation/java/wooteco/prolog/docu/ArticleDocumentation.java b/backend/src/documentation/java/wooteco/prolog/docu/ArticleDocumentation.java new file mode 100644 index 000000000..e872f5074 --- /dev/null +++ b/backend/src/documentation/java/wooteco/prolog/docu/ArticleDocumentation.java @@ -0,0 +1,40 @@ +package wooteco.prolog.docu; + +import static io.netty.handler.codec.http.HttpHeaders.Values.APPLICATION_JSON; +import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import io.restassured.module.mockmvc.response.ValidatableMockMvcResponse; +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import wooteco.prolog.NewDocumentation; +import wooteco.prolog.article.application.ArticleService; +import wooteco.prolog.article.ui.ArticleBookmarkRequest; +import wooteco.prolog.article.ui.ArticleController; + +@WebMvcTest(controllers = ArticleController.class) +public class ArticleDocumentation extends NewDocumentation { + + @MockBean + private ArticleService articleService; + + @Test + void 아티클에_북마크를_변경한다() { + //given, when + final ArticleBookmarkRequest bookmarkRequest = new ArticleBookmarkRequest(true); + + final ValidatableMockMvcResponse response = given + .header("Authorization", "Bearer " + accessToken) + .contentType(APPLICATION_JSON) + .body(bookmarkRequest) + .when().put("/articles/{bookmark-id}/bookmark", 1L) + .then().log().all(); + + //then + response.expect(status().isOk()); + + //docs + response.apply(document("article/bookmark")); + } +} diff --git a/backend/src/main/java/wooteco/prolog/article/ui/ArticleBookmarkRequest.java b/backend/src/main/java/wooteco/prolog/article/ui/ArticleBookmarkRequest.java new file mode 100644 index 000000000..f55f74a35 --- /dev/null +++ b/backend/src/main/java/wooteco/prolog/article/ui/ArticleBookmarkRequest.java @@ -0,0 +1,15 @@ +package wooteco.prolog.article.ui; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public class ArticleBookmarkRequest { + + private final Boolean bookmark; + + public ArticleBookmarkRequest() { + this(null); + } +} From ff26beea608ce1e01c8ec405ca279b8ca97965f2 Mon Sep 17 00:00:00 2001 From: hae-on Date: Wed, 13 Sep 2023 18:04:30 +0900 Subject: [PATCH 03/71] =?UTF-8?q?feat:=20=EC=B9=B4=ED=85=8C=EA=B3=A0?= =?UTF-8?q?=EB=A6=AC=20=ED=95=84=ED=84=B0=20ui=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: 이도현 --- .../src/components/Controls/SelectBox.tsx | 4 +- frontend/src/pages/ArticleListPage/index.tsx | 55 +++++++++++++++++-- 2 files changed, 53 insertions(+), 6 deletions(-) diff --git a/frontend/src/components/Controls/SelectBox.tsx b/frontend/src/components/Controls/SelectBox.tsx index 7a0fa1edb..4e73a86c2 100644 --- a/frontend/src/components/Controls/SelectBox.tsx +++ b/frontend/src/components/Controls/SelectBox.tsx @@ -46,6 +46,7 @@ interface SelectBoxProps { } const SelectBox: React.VFC = ({ + isClearable = true, isMulti = false, options, placeholder, @@ -75,7 +76,7 @@ const SelectBox: React.VFC = ({ `} > + + ); +}; + +export default ArticleBookmarkFilter; + +const ArticleBookmarkFilterContainer = styled.div` + width: 100px; + border: 1px solid black; + border-radius: 5px; + font-size: 1.5rem; +`; diff --git a/frontend/src/components/Article/ArticleList.tsx b/frontend/src/components/Article/ArticleList.tsx index 5aebbfe49..59fd386ce 100644 --- a/frontend/src/components/Article/ArticleList.tsx +++ b/frontend/src/components/Article/ArticleList.tsx @@ -1,13 +1,12 @@ import * as Styled from './ArticleList.style'; import Article from './Article'; -import { useGetRequestArticleQuery } from '../../hooks/queries/article'; +import { ArticleType } from '../../models/Article'; -const ArticleList = () => { - const { data: articles, isLoading, isError } = useGetRequestArticleQuery(); - - if (isLoading) return
loading...
; - if (isError) return
error...
; +interface ArticleListProps { + articles: ArticleType[]; +} +const ArticleList = ({ articles }: ArticleListProps) => { return ( {articles?.map((article) => ( diff --git a/frontend/src/hooks/queries/article.ts b/frontend/src/hooks/queries/article.ts index cc4eded62..f62cd49f8 100644 --- a/frontend/src/hooks/queries/article.ts +++ b/frontend/src/hooks/queries/article.ts @@ -1,20 +1,20 @@ import { useMutation, useQuery, useQueryClient } from 'react-query'; import { - requestGetArticles, + requestGetFilteredArticle, requestPostArticles, requestPutArticleBookmark, } from '../../apis/articles'; -import { ArticleType } from '../../models/Article'; +import { ArticleType, CourseFilter } from '../../models/Article'; import { ERROR_MESSAGE } from '../../constants'; import { SUCCESS_MESSAGE } from '../../constants/message'; const QUERY_KEY = { - articles: 'articles', + filteredArticles: 'filteredArticles', }; -export const useGetRequestArticleQuery = () => { - return useQuery([QUERY_KEY.articles], async () => { - const response = await requestGetArticles(); +export const useGetFilteredArticleQuery = (course: string, bookmark: boolean) => { + return useQuery([QUERY_KEY.filteredArticles], async () => { + const response = await requestGetFilteredArticle(course, bookmark); return response.data; }); @@ -25,7 +25,7 @@ export const usePostArticlesMutation = () => { return useMutation(requestPostArticles, { onSuccess: () => { - queryClient.invalidateQueries([QUERY_KEY.articles]); + queryClient.invalidateQueries([QUERY_KEY.filteredArticles]); alert(SUCCESS_MESSAGE.CREATE_ARTICLE); }, onError: () => { diff --git a/frontend/src/mocks/db/articles.json b/frontend/src/mocks/db/articles.json index cac0d4f16..182d5e558 100644 --- a/frontend/src/mocks/db/articles.json +++ b/frontend/src/mocks/db/articles.json @@ -3,6 +3,7 @@ "id": 1, "userName": "해온", "title": "Axios", + "isBookMarked": true, "url": "https://hae-on.tistory.com/104", "createdAt": "2023-07-24 18:18", "imageUrl": "https://plus.unsplash.com/premium_photo-1682088845396-1b310a002302?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8MXx8JUVDJTk1JTg0JUVEJThCJUIwJUVEJTgxJUI0fGVufDB8fDB8fHww&auto=format&fit=crop&w=500&q=60" @@ -11,6 +12,7 @@ "id": 2, "userName": "패트릭", "title": "CORS", + "isBookMarked": false, "url": "https://pgccoding.tistory.com/66", "createdAt": "2023-07-24 18:18", "imageUrl": "https://plus.unsplash.com/premium_photo-1682088845396-1b310a002302?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8MXx8JUVDJTk1JTg0JUVEJThCJUIwJUVEJTgxJUI0fGVufDB8fDB8fHww&auto=format&fit=crop&w=500&q=60" @@ -19,6 +21,7 @@ "id": 3, "userName": "패트릭", "title": "CORS", + "isBookMarked": true, "url": "https://pgccoding.tistory.com/66", "createdAt": "2023-07-24 18:18", "imageUrl": "https://plus.unsplash.com/premium_photo-1682088845396-1b310a002302?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8MXx8JUVDJTk1JTg0JUVEJThCJUIwJUVEJTgxJUI0fGVufDB8fDB8fHww&auto=format&fit=crop&w=500&q=60" @@ -27,6 +30,7 @@ "id": 4, "userName": "패트릭", "title": "CORS", + "isBookMarked": false, "url": "https://pgccoding.tistory.com/66", "createdAt": "2023-07-24 18:18", "imageUrl": "https://plus.unsplash.com/premium_photo-1682088845396-1b310a002302?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8MXx8JUVDJTk1JTg0JUVEJThCJUIwJUVEJTgxJUI0fGVufDB8fDB8fHww&auto=format&fit=crop&w=500&q=60" diff --git a/frontend/src/mocks/handlers/articles.ts b/frontend/src/mocks/handlers/articles.ts index 9419615fb..4ea38b898 100644 --- a/frontend/src/mocks/handlers/articles.ts +++ b/frontend/src/mocks/handlers/articles.ts @@ -24,6 +24,7 @@ export const articlesHandler = [ title: '직렬화, 역직렬화는 무엇일까?', url: 'https://think0wise.tistory.com/107', createdAt: '2023-07-08 16:48', + isBookMarked: false, imageUrl: 'https://plus.unsplash.com/premium_photo-1682088845396-1b310a002302?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8MXx8JUVDJTk1JTg0JUVEJThCJUIwJUVEJTgxJUI0fGVufDB8fDB8fHww&auto=format&fit=crop&w=500&q=60', }; @@ -35,4 +36,13 @@ export const articlesHandler = [ rest.put(`${BASE_URL}/articles/:articleId/bookmark`, (req, res, ctx) => { return res(ctx.status(200)); }), + + rest.get(`${BASE_URL}/articles/filter`, (req, res, ctx) => { + const course = req.url.searchParams.get('course'); + const bookmark = req.url.searchParams.get('bookmark'); + + const filteredArticle = articles.filter((article) => bookmark === String(article.isBookMarked)); + + return res(ctx.status(200), ctx.json(filteredArticle)); + }), ]; diff --git a/frontend/src/models/Article.ts b/frontend/src/models/Article.ts index 1f194f7a5..4bce9855f 100644 --- a/frontend/src/models/Article.ts +++ b/frontend/src/models/Article.ts @@ -8,6 +8,7 @@ export interface ArticleType { id: number; userName: string; title: string; + isBookMarked: false; url: string; createdAt: string; imageUrl: string; @@ -26,3 +27,7 @@ export interface ArticleBookmarkPutRequest { articleId: number; bookmark: boolean; } + +export type Course = '프론트엔드' | '백엔드' | '안드로이드'; + +export type CourseFilter = Course | '전체보기'; diff --git a/frontend/src/pages/ArticleListPage/index.tsx b/frontend/src/pages/ArticleListPage/index.tsx index e4f55745c..2987f7870 100644 --- a/frontend/src/pages/ArticleListPage/index.tsx +++ b/frontend/src/pages/ArticleListPage/index.tsx @@ -8,8 +8,10 @@ import { COLOR, PATH } from '../../constants'; import styled from '@emotion/styled'; import { MainContentStyle } from '../../PageRouter'; import SelectBox from '../../components/Controls/SelectBox'; -import { useState } from 'react'; +import { ChangeEventHandler, useState } from 'react'; import { css } from '@emotion/react'; +import ArticleBookmarkFilter from '../../components/Article/ArticleBookmarkFIlter'; +import { useGetFilteredArticleQuery } from '../../hooks/queries/article'; const CATEGORY_OPTIONS = [ { value: '전체보기', label: '전체보기' }, @@ -23,10 +25,22 @@ type CategoryOptions = typeof CATEGORY_OPTIONS[number]; const ArticleListPage = () => { const history = useHistory(); const goNewArticlePage = () => history.push(PATH.NEW_ARTICLE); - const [selectedOption, setSelectedOption] = useState(CATEGORY_OPTIONS[0]); + const [selectedCourse, setSelectedCourse] = useState(CATEGORY_OPTIONS[0]); + const [checked, setChecked] = useState(false); + + const { data: filteredArticles = [], refetch: getFilteredArticles } = useGetFilteredArticleQuery( + selectedCourse.value, + checked + ); const changeFilterOption: (option: { value: string; label: string }) => void = (option) => { - setSelectedOption(option); + setSelectedCourse(option); + // getFilteredArticles(); + }; + + const handleCheckBookmark: React.ChangeEventHandler = (e) => { + setChecked(e.currentTarget.checked); + // getFilteredArticles(); }; return ( @@ -35,12 +49,13 @@ const ArticleListPage = () => { + - + ); }; From 5e09e9933f5c7d04f98704347bfec980bbdd55c1 Mon Sep 17 00:00:00 2001 From: hong-sile Date: Fri, 15 Sep 2023 17:56:00 +0900 Subject: [PATCH 13/71] =?UTF-8?q?refactor:=20=EC=95=84=ED=8B=B0=ED=81=B4?= =?UTF-8?q?=EA=B3=BC=20=EB=B6=81=EB=A7=88=ED=81=AC=20=EC=9D=BC=EB=8C=80?= =?UTF-8?q?=EB=8B=A4=20=EB=8B=A8=EB=B0=A9=ED=96=A5=EC=97=90=EC=84=9C=20?= =?UTF-8?q?=EB=8B=A4=EB=8C=80=EC=9D=BC=20=EC=96=91=EB=B0=A9=ED=96=A5?= =?UTF-8?q?=EC=9C=BC=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../prolog/article/domain/Article.java | 2 +- .../article/domain/ArticleBookmark.java | 15 +++++--- .../article/domain/ArticleBookmarks.java | 3 +- .../domain/repository/ArticleRepository.java | 9 +++-- .../article/domain/ArticleBookmarkTest.java | 12 +++++-- .../article/domain/ArticleBookmarksTest.java | 35 ++++++++++++++----- 6 files changed, 54 insertions(+), 22 deletions(-) diff --git a/backend/src/main/java/wooteco/prolog/article/domain/Article.java b/backend/src/main/java/wooteco/prolog/article/domain/Article.java index cca3e0575..e115c85f0 100644 --- a/backend/src/main/java/wooteco/prolog/article/domain/Article.java +++ b/backend/src/main/java/wooteco/prolog/article/domain/Article.java @@ -68,7 +68,7 @@ public void update(final String title, final String url) { } public void addBookmark(final Member member) { - final ArticleBookmark articleBookmark = new ArticleBookmark(id, member.getId()); + final ArticleBookmark articleBookmark = new ArticleBookmark(this, member.getId()); articleBookmarks.addBookmark(articleBookmark); } diff --git a/backend/src/main/java/wooteco/prolog/article/domain/ArticleBookmark.java b/backend/src/main/java/wooteco/prolog/article/domain/ArticleBookmark.java index 2b24b519e..c08e91e92 100644 --- a/backend/src/main/java/wooteco/prolog/article/domain/ArticleBookmark.java +++ b/backend/src/main/java/wooteco/prolog/article/domain/ArticleBookmark.java @@ -2,9 +2,12 @@ import java.util.Objects; import javax.persistence.Entity; +import javax.persistence.FetchType; import javax.persistence.GeneratedValue; import javax.persistence.GenerationType; import javax.persistence.Id; +import javax.persistence.JoinColumn; +import javax.persistence.ManyToOne; import javax.persistence.Table; import lombok.AccessLevel; import lombok.NoArgsConstructor; @@ -18,18 +21,20 @@ public class ArticleBookmark { @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; - private Long articleId; + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "article_id") + private Article article; private Long memberId; - private ArticleBookmark(final Long id, final Long articleId, final Long memberId) { + private ArticleBookmark(final Long id, final Article article, final Long memberId) { this.id = id; - this.articleId = articleId; + this.article = article; this.memberId = memberId; } - public ArticleBookmark(final Long articleId, final Long memberId) { - this(null, articleId, memberId); + public ArticleBookmark(final Article article, final Long memberId) { + this(null, article, memberId); } public boolean isOwner(final Long memberId) { diff --git a/backend/src/main/java/wooteco/prolog/article/domain/ArticleBookmarks.java b/backend/src/main/java/wooteco/prolog/article/domain/ArticleBookmarks.java index fc9d2a3c0..7d332d319 100644 --- a/backend/src/main/java/wooteco/prolog/article/domain/ArticleBookmarks.java +++ b/backend/src/main/java/wooteco/prolog/article/domain/ArticleBookmarks.java @@ -3,7 +3,6 @@ import java.util.ArrayList; import java.util.List; import javax.persistence.Embeddable; -import javax.persistence.FetchType; import javax.persistence.OneToMany; import org.hibernate.annotations.Cascade; import org.hibernate.annotations.CascadeType; @@ -11,7 +10,7 @@ @Embeddable public class ArticleBookmarks { - @OneToMany(mappedBy = "article", fetch = FetchType.LAZY) + @OneToMany(mappedBy = "article") @Cascade(value = {CascadeType.PERSIST, CascadeType.DELETE}) private List articleBookmarks; diff --git a/backend/src/main/java/wooteco/prolog/article/domain/repository/ArticleRepository.java b/backend/src/main/java/wooteco/prolog/article/domain/repository/ArticleRepository.java index b79ca8d1a..257c008b7 100644 --- a/backend/src/main/java/wooteco/prolog/article/domain/repository/ArticleRepository.java +++ b/backend/src/main/java/wooteco/prolog/article/domain/repository/ArticleRepository.java @@ -1,11 +1,16 @@ package wooteco.prolog.article.domain.repository; +import java.util.List; +import java.util.Optional; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; import wooteco.prolog.article.domain.Article; -import java.util.List; - public interface ArticleRepository extends JpaRepository { List
findAllByOrderByCreatedAtDesc(); + + @Query("select a from Article a join fetch ArticleBookmark where a.id = :id") + Optional
findById(@Param("id") final Long id); } diff --git a/backend/src/test/java/wooteco/prolog/article/domain/ArticleBookmarkTest.java b/backend/src/test/java/wooteco/prolog/article/domain/ArticleBookmarkTest.java index cff4a2e5f..8f9914730 100644 --- a/backend/src/test/java/wooteco/prolog/article/domain/ArticleBookmarkTest.java +++ b/backend/src/test/java/wooteco/prolog/article/domain/ArticleBookmarkTest.java @@ -4,6 +4,8 @@ import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; +import wooteco.prolog.member.domain.Member; +import wooteco.prolog.member.domain.Role; class ArticleBookmarkTest { @@ -11,11 +13,15 @@ class ArticleBookmarkTest { @Test void isOwner() { //given - final Long memberId = 2L; - final ArticleBookmark articleBookmark = new ArticleBookmark(1L, memberId); + final Member member = new Member(11L, "username", "nickname", Role.CREW, 101L, "https://"); + final Title title = new Title("title"); + final Url url = new Url("url"); + final ImageUrl imageUrl = new ImageUrl("imageUrl"); + final Article article = new Article(member, title, url, imageUrl); + final ArticleBookmark articleBookmark = new ArticleBookmark(article, member.getId()); //when - final Boolean actual = articleBookmark.isOwner(memberId); + final Boolean actual = articleBookmark.isOwner(member.getId()); //then assertThat(actual) diff --git a/backend/src/test/java/wooteco/prolog/article/domain/ArticleBookmarksTest.java b/backend/src/test/java/wooteco/prolog/article/domain/ArticleBookmarksTest.java index a88e8e05e..9fa73c30e 100644 --- a/backend/src/test/java/wooteco/prolog/article/domain/ArticleBookmarksTest.java +++ b/backend/src/test/java/wooteco/prolog/article/domain/ArticleBookmarksTest.java @@ -2,23 +2,40 @@ import static org.assertj.core.api.Assertions.assertThat; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; +import wooteco.prolog.member.domain.Member; +import wooteco.prolog.member.domain.Role; class ArticleBookmarksTest { + private Member member; + private Title title; + private Url url; + private ImageUrl imageUrl; + + @BeforeEach + void setUp() { + member = new Member(11L, "username", "nickname", Role.CREW, 101L, "https://"); + title = new Title("title"); + url = new Url("url"); + imageUrl = new ImageUrl("imageUrl"); + } + @Test @DisplayName("ArticleBookmark를 추가할 수 있다.") void addArticleBookmark() { //given final ArticleBookmarks articleBookmarks = new ArticleBookmarks(); - final ArticleBookmark articleBookmark = new ArticleBookmark(1L, 2L); + final Article article = new Article(member, title, url, imageUrl); + final ArticleBookmark articleBookmark = new ArticleBookmark(article, member.getId()); //when articleBookmarks.addBookmark(articleBookmark); //then - assertThat(articleBookmarks.containBookmark(2L)) + assertThat(articleBookmarks.containBookmark(member.getId())) .isTrue(); } @@ -27,15 +44,15 @@ void addArticleBookmark() { void removeArticleBookmark() { //given final ArticleBookmarks articleBookmarks = new ArticleBookmarks(); - final Long memberId = 2L; - final ArticleBookmark articleBookmark = new ArticleBookmark(1L, memberId); + final Article article = new Article(member, title, url, imageUrl); + final ArticleBookmark articleBookmark = new ArticleBookmark(article, member.getId()); articleBookmarks.addBookmark(articleBookmark); //when - articleBookmarks.removeBookmark(memberId); + articleBookmarks.removeBookmark(member.getId()); //then - assertThat(articleBookmarks.containBookmark(memberId)) + assertThat(articleBookmarks.containBookmark(member.getId())) .isFalse(); } @@ -44,12 +61,12 @@ void removeArticleBookmark() { void containBookmark() { //given final ArticleBookmarks articleBookmarks = new ArticleBookmarks(); - final Long memberId = 2L; - final ArticleBookmark articleBookmark = new ArticleBookmark(1L, memberId); + final Article article = new Article(member, title, url, imageUrl); + final ArticleBookmark articleBookmark = new ArticleBookmark(article, member.getId()); articleBookmarks.addBookmark(articleBookmark); //when - final Boolean actual = articleBookmarks.containBookmark(memberId); + final Boolean actual = articleBookmarks.containBookmark(member.getId()); //then assertThat(actual) From 2a6cadcb1c38fba55496229b2f25ff1284c6af1a Mon Sep 17 00:00:00 2001 From: hong-sile Date: Fri, 15 Sep 2023 22:03:50 +0900 Subject: [PATCH 14/71] =?UTF-8?q?feat:=20article=5Fbookmark=20flyway?= =?UTF-8?q?=EB=A5=BC=20=EC=9C=84=ED=95=9C=20sql=EB=AC=B8=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../migration/prod/V6_create_table_article_bookmark.sql | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 backend/src/main/resources/db/migration/prod/V6_create_table_article_bookmark.sql diff --git a/backend/src/main/resources/db/migration/prod/V6_create_table_article_bookmark.sql b/backend/src/main/resources/db/migration/prod/V6_create_table_article_bookmark.sql new file mode 100644 index 000000000..fc5b4f786 --- /dev/null +++ b/backend/src/main/resources/db/migration/prod/V6_create_table_article_bookmark.sql @@ -0,0 +1,8 @@ +create table if not exists prolog.article_bookmark +( + id bigint auto_increment primary key, + article_id bigint not null, + member_id bigint not null +) ENGINE = InnoDB + DEFAULT CHARSET = utf8mb4 + COLLATE = utf8mb4_0900_ai_ci; From 8b705cb287a74b5237af5b3af5d6655be7026c42 Mon Sep 17 00:00:00 2001 From: hong-sile Date: Fri, 15 Sep 2023 22:04:16 +0900 Subject: [PATCH 15/71] =?UTF-8?q?refactor:=20fetch=20join=EC=9C=BC?= =?UTF-8?q?=EB=A1=9C=20=EA=B0=80=EC=A0=B8=EC=98=A4=EB=8A=94=20=EC=A1=B0?= =?UTF-8?q?=ED=9A=8C=20=EB=A9=94=EC=84=9C=EB=93=9C=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../prolog/article/application/ArticleService.java | 2 +- .../article/domain/repository/ArticleRepository.java | 4 ++-- .../wooteco/prolog/article/ui/ArticleController.java | 9 +++++++++ 3 files changed, 12 insertions(+), 3 deletions(-) diff --git a/backend/src/main/java/wooteco/prolog/article/application/ArticleService.java b/backend/src/main/java/wooteco/prolog/article/application/ArticleService.java index bddc36e9e..ede45aa67 100644 --- a/backend/src/main/java/wooteco/prolog/article/application/ArticleService.java +++ b/backend/src/main/java/wooteco/prolog/article/application/ArticleService.java @@ -69,7 +69,7 @@ public void delete(final Long id, final LoginMember loginMember) { @Transactional public void bookmarkArticle(final Long id, final LoginMember loginMember, final Boolean checked) { - final Article article = articleRepository.findById(id) + final Article article = articleRepository.findFetchById(id) .orElseThrow(() -> new BadRequestException(ARTICLE_NOT_FOUND_EXCEPTION)); final Member member = memberService.findById(loginMember.getId()); diff --git a/backend/src/main/java/wooteco/prolog/article/domain/repository/ArticleRepository.java b/backend/src/main/java/wooteco/prolog/article/domain/repository/ArticleRepository.java index 257c008b7..df5efe865 100644 --- a/backend/src/main/java/wooteco/prolog/article/domain/repository/ArticleRepository.java +++ b/backend/src/main/java/wooteco/prolog/article/domain/repository/ArticleRepository.java @@ -11,6 +11,6 @@ public interface ArticleRepository extends JpaRepository { List
findAllByOrderByCreatedAtDesc(); - @Query("select a from Article a join fetch ArticleBookmark where a.id = :id") - Optional
findById(@Param("id") final Long id); + @Query("select a from Article a join fetch a.articleBookmarks where a.id = :id") + Optional
findFetchById(@Param("id") final Long id); } diff --git a/backend/src/main/java/wooteco/prolog/article/ui/ArticleController.java b/backend/src/main/java/wooteco/prolog/article/ui/ArticleController.java index f88058c12..1cacde254 100644 --- a/backend/src/main/java/wooteco/prolog/article/ui/ArticleController.java +++ b/backend/src/main/java/wooteco/prolog/article/ui/ArticleController.java @@ -13,6 +13,7 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import wooteco.prolog.article.application.ArticleService; +import wooteco.prolog.article.domain.ArticleBookmark; import wooteco.prolog.login.domain.AuthMemberPrincipal; import wooteco.prolog.login.ui.LoginMember; @@ -50,4 +51,12 @@ public ResponseEntity deleteArticle(@PathVariable final Long id, articleService.delete(id, member); return ResponseEntity.noContent().build(); } + + @PutMapping("/{id}/bookmark") + public ResponseEntity bookmarkArticle(@PathVariable final Long id, + @AuthMemberPrincipal final LoginMember member, + @RequestBody final ArticleBookmarkRequest request) { + articleService.bookmarkArticle(id, member, request.getBookmark()); + return ResponseEntity.ok().build(); + } } From d340847a7e23a6cd0223f7ca3bf9abc6c0a80d63 Mon Sep 17 00:00:00 2001 From: hong-sile Date: Fri, 15 Sep 2023 22:04:44 +0900 Subject: [PATCH 16/71] =?UTF-8?q?test:=20=EC=95=84=ED=8B=B0=ED=81=B4=20?= =?UTF-8?q?=EC=8A=A4=ED=81=AC=EB=9E=A9=20=EB=93=B1=EB=A1=9D=20=EC=9D=B8?= =?UTF-8?q?=EC=88=98=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../prolog/steps/ArticleStepDefinitions.java | 23 ++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/backend/src/acceptanceTest/java/wooteco/prolog/steps/ArticleStepDefinitions.java b/backend/src/acceptanceTest/java/wooteco/prolog/steps/ArticleStepDefinitions.java index afd170630..cbbb977c7 100644 --- a/backend/src/acceptanceTest/java/wooteco/prolog/steps/ArticleStepDefinitions.java +++ b/backend/src/acceptanceTest/java/wooteco/prolog/steps/ArticleStepDefinitions.java @@ -1,6 +1,7 @@ package wooteco.prolog.steps; import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; import static wooteco.prolog.fixtures.ArticleFixture.ARTICLE_REQUEST1; import static wooteco.prolog.fixtures.ArticleFixture.ARTICLE_REQUEST2; @@ -10,6 +11,7 @@ import java.util.List; import org.springframework.http.HttpStatus; import wooteco.prolog.AcceptanceSteps; +import wooteco.prolog.article.ui.ArticleBookmarkRequest; import wooteco.prolog.article.ui.ArticleResponse; import wooteco.prolog.article.ui.ArticleUrlResponse; @@ -22,7 +24,7 @@ public class ArticleStepDefinitions extends AcceptanceSteps { context.invokeHttpPostWithToken("/articles", ARTICLE_REQUEST2); } - @Given("아티클을 작성하고") + @Given("아티클이 작성되어 있고") @When("아티클을 작성하면") public void 아티클을_작성하면() { context.invokeHttpPostWithToken("/articles", ARTICLE_REQUEST1); @@ -96,4 +98,23 @@ public class ArticleStepDefinitions extends AcceptanceSteps { .usingRecursiveComparison() .isEqualTo(expected); } + + @When("{long}번 아티클에 북마크 요청을 보내면") + public void 아티클에_북마크_요청을_보내면(final Long articleId) { + //final String articleUrl = context.response.header("Location"); + final ArticleBookmarkRequest request = new ArticleBookmarkRequest(true); + context.invokeHttpPutWithToken( + String.format("/articles/%d/bookmark", articleId), + request + ); + } + + @Then("아티클에 북마크가 등록된다") + public void 아티클에_북마크가_등록된다() { + final int statusCode = context.response.statusCode(); + assertAll( + () -> assertThat(statusCode).isEqualTo(HttpStatus.OK.value()) + ); + //아티클 단건 조회 변경 시, 단건 조회로 bookmark 여부까지 검증해보기 + } } From df1670c18e0b1c048ecce2b9b667805feab5e093 Mon Sep 17 00:00:00 2001 From: hong-sile Date: Fri, 15 Sep 2023 22:26:26 +0900 Subject: [PATCH 17/71] =?UTF-8?q?fix:=20flyway=20=EC=98=A4=EB=A5=98=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...article_bookmark.sql => V6__create_table_article_bookmark.sql} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename backend/src/main/resources/db/migration/prod/{V6_create_table_article_bookmark.sql => V6__create_table_article_bookmark.sql} (100%) diff --git a/backend/src/main/resources/db/migration/prod/V6_create_table_article_bookmark.sql b/backend/src/main/resources/db/migration/prod/V6__create_table_article_bookmark.sql similarity index 100% rename from backend/src/main/resources/db/migration/prod/V6_create_table_article_bookmark.sql rename to backend/src/main/resources/db/migration/prod/V6__create_table_article_bookmark.sql From e93e72e65ea13463181da32cab861870c0bf95b3 Mon Sep 17 00:00:00 2001 From: hong-sile Date: Fri, 15 Sep 2023 22:33:24 +0900 Subject: [PATCH 18/71] =?UTF-8?q?test:=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=EC=98=A4=EB=A5=98=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../prolog/article/application/ArticleServiceTest.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/backend/src/test/java/wooteco/prolog/article/application/ArticleServiceTest.java b/backend/src/test/java/wooteco/prolog/article/application/ArticleServiceTest.java index 12b4712c8..a93382d44 100644 --- a/backend/src/test/java/wooteco/prolog/article/application/ArticleServiceTest.java +++ b/backend/src/test/java/wooteco/prolog/article/application/ArticleServiceTest.java @@ -183,7 +183,7 @@ void delete_fail_InvalidArticleAuthorException() { } @Nested - @DisplayName("아티클의 북마크 상태를 바굴 수 있다.") + @DisplayName("아티클의 북마크 상태를 바꿀 수 있다.") class bookmarkArticle { @DisplayName("아티클의 북마크를 추가한다.") @@ -197,7 +197,7 @@ void add() { final Long articleId = 3L; final LoginMember loginMember = new LoginMember(member.getId(), MEMBER); - when(articleRepository.findById(articleId)).thenReturn(Optional.of(article)); + when(articleRepository.findFetchById(articleId)).thenReturn(Optional.of(article)); when(memberService.findById(member.getId())).thenReturn(member); //when @@ -220,7 +220,7 @@ void remove() { final Long articleId = 3L; final LoginMember loginMember = new LoginMember(member.getId(), MEMBER); - when(articleRepository.findById(articleId)).thenReturn(Optional.of(article)); + when(articleRepository.findFetchById(articleId)).thenReturn(Optional.of(article)); when(memberService.findById(member.getId())).thenReturn(member); //when From 7579d21970e4a434d49caaa523e3c90de8d8f688 Mon Sep 17 00:00:00 2001 From: hong-sile Date: Mon, 18 Sep 2023 15:29:03 +0900 Subject: [PATCH 19/71] =?UTF-8?q?feat:=20=EC=95=84=ED=8B=B0=ED=81=B4=20?= =?UTF-8?q?=EC=A2=8B=EC=95=84=EC=9A=94=20=EB=93=B1=EB=A1=9D=20=EC=9D=B8?= =?UTF-8?q?=EC=88=98=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../prolog/steps/ArticleStepDefinitions.java | 21 ++++++++++++++++++- .../resources/wooteco/prolog/article.feature | 5 +++++ 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/backend/src/acceptanceTest/java/wooteco/prolog/steps/ArticleStepDefinitions.java b/backend/src/acceptanceTest/java/wooteco/prolog/steps/ArticleStepDefinitions.java index cbbb977c7..4b53ca4c3 100644 --- a/backend/src/acceptanceTest/java/wooteco/prolog/steps/ArticleStepDefinitions.java +++ b/backend/src/acceptanceTest/java/wooteco/prolog/steps/ArticleStepDefinitions.java @@ -115,6 +115,25 @@ public class ArticleStepDefinitions extends AcceptanceSteps { assertAll( () -> assertThat(statusCode).isEqualTo(HttpStatus.OK.value()) ); - //아티클 단건 조회 변경 시, 단건 조회로 bookmark 여부까지 검증해보기 + //아티클 단건 조회에 북마크 여부가 추가되면 단건 조회로 bookmark 여부까지 검증해보기 + } + + @When("{long}번 아티클에 좋아요 요청을 보내면") + public void 아티클에_좋아요_요청을_보내면(final Long articleId) { + //final String articleUrl = context.response.header("Location"); + final ArticleBookmarkRequest request = new ArticleBookmarkRequest(true); + context.invokeHttpPutWithToken( + String.format("/articles/%d/likes", articleId), + request + ); + } + + @Then("아티클에 좋아요가 등록된다") + public void 아티클에_좋아요가_등록된다() { + final int statusCode = context.response.statusCode(); + assertAll( + () -> assertThat(statusCode).isEqualTo(HttpStatus.OK.value()) + ); + //아티클 단건 조회에 좋아요 여부가 추가되면 단건 조회로 likes 여부까지 검증해보기 } } diff --git a/backend/src/acceptanceTest/resources/wooteco/prolog/article.feature b/backend/src/acceptanceTest/resources/wooteco/prolog/article.feature index 631295ce9..0c21cddc5 100644 --- a/backend/src/acceptanceTest/resources/wooteco/prolog/article.feature +++ b/backend/src/acceptanceTest/resources/wooteco/prolog/article.feature @@ -31,3 +31,8 @@ Feature: 아티클 관련 기능 Given 아티클이 작성되어 있고 When 1번 아티클에 북마크 요청을 보내면 Then 아티클에 북마크가 등록된다 + + Scenario: 아티클에 좋아요를 등록하기 + Given 아티클이 작성되어 있고 + When 1번 아티클에 좋아요 요청을 보내면 + Then 아티클에 좋아요가 등록된다 From 294cf4932ef594b2c8dac194ab5e90082fa12b6b Mon Sep 17 00:00:00 2001 From: hong-sile Date: Mon, 18 Sep 2023 15:36:57 +0900 Subject: [PATCH 20/71] =?UTF-8?q?feat:=20=EC=95=84=ED=8B=B0=ED=81=B4=20?= =?UTF-8?q?=EB=AC=B8=EC=84=9C=ED=99=94=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/src/documentation/adoc/article.adoc | 10 ++++++++++ .../prolog/docu/ArticleDocumentation.java | 20 +++++++++++++++++++ .../article/application/ArticleService.java | 4 ++++ .../prolog/article/ui/ArticleController.java | 9 ++++++++- .../article/ui/ArticleLikesRequest.java | 15 ++++++++++++++ 5 files changed, 57 insertions(+), 1 deletion(-) create mode 100644 backend/src/main/java/wooteco/prolog/article/ui/ArticleLikesRequest.java diff --git a/backend/src/documentation/adoc/article.adoc b/backend/src/documentation/adoc/article.adoc index 4c913ca77..052ed0ea2 100644 --- a/backend/src/documentation/adoc/article.adoc +++ b/backend/src/documentation/adoc/article.adoc @@ -10,3 +10,13 @@ include::{snippets}/article/bookmark/http-request.adoc[] ==== Response include::{snippets}/article/bookmark/http-response.adoc[] + +=== 아티클 좋아요 추가 + +==== Request + +include::{snippets}/article/likes/http-request.adoc[] + +==== Response + +include::{snippets}/article/likes/http-response.adoc[] diff --git a/backend/src/documentation/java/wooteco/prolog/docu/ArticleDocumentation.java b/backend/src/documentation/java/wooteco/prolog/docu/ArticleDocumentation.java index e872f5074..fdc2f2b61 100644 --- a/backend/src/documentation/java/wooteco/prolog/docu/ArticleDocumentation.java +++ b/backend/src/documentation/java/wooteco/prolog/docu/ArticleDocumentation.java @@ -12,6 +12,7 @@ import wooteco.prolog.article.application.ArticleService; import wooteco.prolog.article.ui.ArticleBookmarkRequest; import wooteco.prolog.article.ui.ArticleController; +import wooteco.prolog.article.ui.ArticleLikesRequest; @WebMvcTest(controllers = ArticleController.class) public class ArticleDocumentation extends NewDocumentation { @@ -37,4 +38,23 @@ public class ArticleDocumentation extends NewDocumentation { //docs response.apply(document("article/bookmark")); } + + @Test + void 아티클에_좋아요를_변경한다() { + //given, when + final ArticleLikesRequest articleLikesRequest = new ArticleLikesRequest(true); + + final ValidatableMockMvcResponse response = given + .header("Authorization", "Bearer " + accessToken) + .contentType(APPLICATION_JSON) + .body(articleLikesRequest) + .when().put("/articles/{bookmark-id}/likes", 1L) + .then().log().all(); + + //then + response.expect(status().isOk()); + + //docs + response.apply(document("article/likes")); + } } diff --git a/backend/src/main/java/wooteco/prolog/article/application/ArticleService.java b/backend/src/main/java/wooteco/prolog/article/application/ArticleService.java index ede45aa67..f63f21a32 100644 --- a/backend/src/main/java/wooteco/prolog/article/application/ArticleService.java +++ b/backend/src/main/java/wooteco/prolog/article/application/ArticleService.java @@ -79,4 +79,8 @@ public void bookmarkArticle(final Long id, final LoginMember loginMember, article.removeBookmark(member); } } + + public void likeArticle(final Long id, final LoginMember member, final Boolean likes) { + + } } diff --git a/backend/src/main/java/wooteco/prolog/article/ui/ArticleController.java b/backend/src/main/java/wooteco/prolog/article/ui/ArticleController.java index 1cacde254..9bf7d2171 100644 --- a/backend/src/main/java/wooteco/prolog/article/ui/ArticleController.java +++ b/backend/src/main/java/wooteco/prolog/article/ui/ArticleController.java @@ -13,7 +13,6 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import wooteco.prolog.article.application.ArticleService; -import wooteco.prolog.article.domain.ArticleBookmark; import wooteco.prolog.login.domain.AuthMemberPrincipal; import wooteco.prolog.login.ui.LoginMember; @@ -59,4 +58,12 @@ public ResponseEntity bookmarkArticle(@PathVariable final Long id, articleService.bookmarkArticle(id, member, request.getBookmark()); return ResponseEntity.ok().build(); } + + @PutMapping("/{id}/likes") + public ResponseEntity likeArticle(@PathVariable final Long id, + @AuthMemberPrincipal final LoginMember member, + @RequestBody final ArticleLikesRequest request) { + articleService.likeArticle(id, member, request.getLikes()); + return ResponseEntity.ok().build(); + } } diff --git a/backend/src/main/java/wooteco/prolog/article/ui/ArticleLikesRequest.java b/backend/src/main/java/wooteco/prolog/article/ui/ArticleLikesRequest.java new file mode 100644 index 000000000..e1e4a9b89 --- /dev/null +++ b/backend/src/main/java/wooteco/prolog/article/ui/ArticleLikesRequest.java @@ -0,0 +1,15 @@ +package wooteco.prolog.article.ui; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public class ArticleLikesRequest { + + private final Boolean likes; + + public ArticleLikesRequest() { + this(null); + } +} From 8415eb899b54484febe13740b36f6f0529649f4e Mon Sep 17 00:00:00 2001 From: hong-sile Date: Mon, 18 Sep 2023 15:17:36 +0900 Subject: [PATCH 21/71] =?UTF-8?q?refactor:=20=EC=95=84=ED=8B=B0=ED=81=B4?= =?UTF-8?q?=20=EC=84=9C=EB=B9=84=EC=8A=A4=EC=97=90=20=EC=9E=88=EB=8A=94=20?= =?UTF-8?q?=EB=A1=9C=EC=A7=81=EC=9D=84=20=EC=95=84=ED=8B=B0=ED=81=B4=20?= =?UTF-8?q?=EB=8F=84=EB=A9=94=EC=9D=B8=EC=9C=BC=EB=A1=9C=20=EC=88=A8?= =?UTF-8?q?=EA=B8=B0=EA=B8=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../article/application/ArticleService.java | 9 +-- .../prolog/article/domain/Article.java | 14 ++++- .../prolog/article/domain/ArticleTest.java | 58 ++++++++++--------- 3 files changed, 46 insertions(+), 35 deletions(-) diff --git a/backend/src/main/java/wooteco/prolog/article/application/ArticleService.java b/backend/src/main/java/wooteco/prolog/article/application/ArticleService.java index f63f21a32..fdce26866 100644 --- a/backend/src/main/java/wooteco/prolog/article/application/ArticleService.java +++ b/backend/src/main/java/wooteco/prolog/article/application/ArticleService.java @@ -68,16 +68,11 @@ public void delete(final Long id, final LoginMember loginMember) { @Transactional public void bookmarkArticle(final Long id, final LoginMember loginMember, - final Boolean checked) { + final Boolean isBookmark) { final Article article = articleRepository.findFetchById(id) .orElseThrow(() -> new BadRequestException(ARTICLE_NOT_FOUND_EXCEPTION)); final Member member = memberService.findById(loginMember.getId()); - - if (TRUE.equals(checked)) { - article.addBookmark(member); - } else { - article.removeBookmark(member); - } + article.setBookmark(member, isBookmark); } public void likeArticle(final Long id, final LoginMember member, final Boolean likes) { diff --git a/backend/src/main/java/wooteco/prolog/article/domain/Article.java b/backend/src/main/java/wooteco/prolog/article/domain/Article.java index e115c85f0..2b2584723 100644 --- a/backend/src/main/java/wooteco/prolog/article/domain/Article.java +++ b/backend/src/main/java/wooteco/prolog/article/domain/Article.java @@ -1,5 +1,7 @@ package wooteco.prolog.article.domain; +import static java.lang.Boolean.TRUE; + import java.time.LocalDateTime; import javax.persistence.Embedded; import javax.persistence.Entity; @@ -67,12 +69,20 @@ public void update(final String title, final String url) { this.url = new Url(url); } - public void addBookmark(final Member member) { + public void setBookmark(final Member member, final Boolean isBookmark) { + if (TRUE.equals(isBookmark)) { + addBookmark(member); + } else { + removeBookmark(member); + } + } + + private void addBookmark(final Member member) { final ArticleBookmark articleBookmark = new ArticleBookmark(this, member.getId()); articleBookmarks.addBookmark(articleBookmark); } - public void removeBookmark(final Member member) { + private void removeBookmark(final Member member) { articleBookmarks.removeBookmark(member.getId()); } } diff --git a/backend/src/test/java/wooteco/prolog/article/domain/ArticleTest.java b/backend/src/test/java/wooteco/prolog/article/domain/ArticleTest.java index 44f9c8cd8..576546c06 100644 --- a/backend/src/test/java/wooteco/prolog/article/domain/ArticleTest.java +++ b/backend/src/test/java/wooteco/prolog/article/domain/ArticleTest.java @@ -7,6 +7,7 @@ import joptsimple.internal.Strings; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import wooteco.prolog.common.exception.BadRequestException; import wooteco.prolog.member.domain.Member; @@ -98,38 +99,43 @@ void update_invalidUrl() { .isInstanceOf(BadRequestException.class); } - @DisplayName("멤버가 아티클을 북마크로 등록한다.") - @Test - void addBookmark() { - //given - final Article article = new Article(member, title, url, imageUrl); + @Nested + @DisplayName("멤버가 아티클의 북마크값을 세팅한다.") + class setBookmark { - //when - article.addBookmark(member); + @DisplayName("멤버가 아티클을 북마크로 등록한다.") + @Test + void addBookmark() { + //given + final Article article = new Article(member, title, url, imageUrl); - //then - final ArticleBookmarks articleBookmarks = article.getArticleBookmarks(); - final boolean contains = articleBookmarks.containBookmark(member.getId()); + //when + article.setBookmark(member, true); - assertThat(contains) - .isTrue(); - } + //then + final ArticleBookmarks articleBookmarks = article.getArticleBookmarks(); + final boolean contains = articleBookmarks.containBookmark(member.getId()); - @DisplayName("멤버가 아티클을 북마크로 등록한다.") - @Test - void removeBookmark() { - //given - final Article article = new Article(member, title, url, imageUrl); - article.addBookmark(member); + assertThat(contains) + .isTrue(); + } - //when - article.removeBookmark(member); + @DisplayName("멤버가 아티클의 북마크를 해제한다.") + @Test + void removeBookmark() { + //given + final Article article = new Article(member, title, url, imageUrl); + article.setBookmark(member, true); - //then - final ArticleBookmarks articleBookmarks = article.getArticleBookmarks(); - final boolean contains = articleBookmarks.containBookmark(member.getId()); + //when + article.setBookmark(member, false); + + //then + final ArticleBookmarks articleBookmarks = article.getArticleBookmarks(); + final boolean contains = articleBookmarks.containBookmark(member.getId()); - assertThat(contains) - .isFalse(); + assertThat(contains) + .isFalse(); + } } } From 6688b9cab78b75a8e999a60a907dd65e5ba69bf7 Mon Sep 17 00:00:00 2001 From: hong-sile Date: Mon, 18 Sep 2023 15:37:49 +0900 Subject: [PATCH 22/71] =?UTF-8?q?test:=20=EC=9E=98=EB=AA=BB=EB=90=9C=20?= =?UTF-8?q?=EB=B3=80=EC=88=98=EB=AA=85=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/wooteco/prolog/docu/ArticleDocumentation.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/backend/src/documentation/java/wooteco/prolog/docu/ArticleDocumentation.java b/backend/src/documentation/java/wooteco/prolog/docu/ArticleDocumentation.java index fdc2f2b61..43832e60e 100644 --- a/backend/src/documentation/java/wooteco/prolog/docu/ArticleDocumentation.java +++ b/backend/src/documentation/java/wooteco/prolog/docu/ArticleDocumentation.java @@ -29,7 +29,7 @@ public class ArticleDocumentation extends NewDocumentation { .header("Authorization", "Bearer " + accessToken) .contentType(APPLICATION_JSON) .body(bookmarkRequest) - .when().put("/articles/{bookmark-id}/bookmark", 1L) + .when().put("/articles/{article-id}/bookmark", 1L) .then().log().all(); //then @@ -48,7 +48,7 @@ public class ArticleDocumentation extends NewDocumentation { .header("Authorization", "Bearer " + accessToken) .contentType(APPLICATION_JSON) .body(articleLikesRequest) - .when().put("/articles/{bookmark-id}/likes", 1L) + .when().put("/articles/{article-id}/likes", 1L) .then().log().all(); //then From c8137f8d34229ad0e6f48598f2ba747941a880c6 Mon Sep 17 00:00:00 2001 From: hong-sile Date: Mon, 18 Sep 2023 15:50:18 +0900 Subject: [PATCH 23/71] =?UTF-8?q?feat:=20=EC=95=84=ED=8B=B0=ED=81=B4=20?= =?UTF-8?q?=EC=A2=8B=EC=95=84=EC=9A=94=20=EC=97=94=ED=8B=B0=ED=8B=B0=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../prolog/article/domain/ArticleLike.java | 43 +++++++++++++++++++ .../prod/V7__create_table_article_like.sql | 8 ++++ .../article/domain/ArticleLikeTest.java | 30 +++++++++++++ 3 files changed, 81 insertions(+) create mode 100644 backend/src/main/java/wooteco/prolog/article/domain/ArticleLike.java create mode 100644 backend/src/main/resources/db/migration/prod/V7__create_table_article_like.sql create mode 100644 backend/src/test/java/wooteco/prolog/article/domain/ArticleLikeTest.java diff --git a/backend/src/main/java/wooteco/prolog/article/domain/ArticleLike.java b/backend/src/main/java/wooteco/prolog/article/domain/ArticleLike.java new file mode 100644 index 000000000..6748ff43c --- /dev/null +++ b/backend/src/main/java/wooteco/prolog/article/domain/ArticleLike.java @@ -0,0 +1,43 @@ +package wooteco.prolog.article.domain; + +import java.util.Objects; +import javax.persistence.Entity; +import javax.persistence.FetchType; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.JoinColumn; +import javax.persistence.ManyToOne; +import javax.persistence.Table; +import lombok.AccessLevel; +import lombok.NoArgsConstructor; + +@Entity +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Table(name = "article_like") +public class ArticleLike { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "article_id") + private Article article; + + private Long memberId; + + private ArticleLike(final Long id, final Article article, final Long memberId) { + this.id = id; + this.article = article; + this.memberId = memberId; + } + + public ArticleLike(final Article article, final Long memberId) { + this(null, article, memberId); + } + + public boolean isOwner(final Long memberId) { + return Objects.equals(this.memberId, memberId); + } +} diff --git a/backend/src/main/resources/db/migration/prod/V7__create_table_article_like.sql b/backend/src/main/resources/db/migration/prod/V7__create_table_article_like.sql new file mode 100644 index 000000000..4fbe72b0e --- /dev/null +++ b/backend/src/main/resources/db/migration/prod/V7__create_table_article_like.sql @@ -0,0 +1,8 @@ +create table if not exists prolog.article_like +( + id bigint auto_increment primary key, + article_id bigint not null, + member_id bigint not null +) ENGINE = InnoDB + DEFAULT CHARSET = utf8mb4 + COLLATE = utf8mb4_0900_ai_ci; diff --git a/backend/src/test/java/wooteco/prolog/article/domain/ArticleLikeTest.java b/backend/src/test/java/wooteco/prolog/article/domain/ArticleLikeTest.java new file mode 100644 index 000000000..c0527dda4 --- /dev/null +++ b/backend/src/test/java/wooteco/prolog/article/domain/ArticleLikeTest.java @@ -0,0 +1,30 @@ +package wooteco.prolog.article.domain; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import wooteco.prolog.member.domain.Member; +import wooteco.prolog.member.domain.Role; + +class ArticleLikeTest { + + @DisplayName("memberId가 동일한지 확인") + @Test + void isOwner() { + //given + final Member member = new Member(11L, "username", "nickname", Role.CREW, 101L, "https://"); + final Title title = new Title("title"); + final Url url = new Url("url"); + final ImageUrl imageUrl = new ImageUrl("imageUrl"); + final Article article = new Article(member, title, url, imageUrl); + final ArticleLike articleLike = new ArticleLike(article, member.getId()); + + //when + final Boolean actual = articleLike.isOwner(member.getId()); + + //then + assertThat(actual) + .isTrue(); + } +} From 694c7b1e669c7cca70642a7543d0aa5e6aaafc78 Mon Sep 17 00:00:00 2001 From: hae-on Date: Mon, 18 Sep 2023 15:53:38 +0900 Subject: [PATCH 24/71] =?UTF-8?q?refactor:=20=EB=B6=81=EB=A7=88=ED=81=AC?= =?UTF-8?q?=20=EB=AA=85=EC=84=B8=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: 이도현 --- frontend/src/apis/articles.ts | 3 +-- frontend/src/mocks/handlers/articles.ts | 6 ++++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/frontend/src/apis/articles.ts b/frontend/src/apis/articles.ts index 4f4f8bd46..b2f1bace5 100644 --- a/frontend/src/apis/articles.ts +++ b/frontend/src/apis/articles.ts @@ -2,7 +2,6 @@ import { client } from '.'; import { ArticleBookmarkPutRequest, ArticleRequest, - CourseFilter, MetaOgRequest, MetaOgResponse, } from '../models/Article'; @@ -20,5 +19,5 @@ export const requestPutArticleBookmark = ({ articleId, bookmark }: ArticleBookma }; export const requestGetFilteredArticle = (course: string, bookmark: boolean) => { - return client.get(`/articles/filter?course=${course}&bookmark=${bookmark}`); + return client.get(`/articles/filter?course=${course}&onlyBookmarked=${bookmark}`); }; diff --git a/frontend/src/mocks/handlers/articles.ts b/frontend/src/mocks/handlers/articles.ts index 4ea38b898..441dae3e9 100644 --- a/frontend/src/mocks/handlers/articles.ts +++ b/frontend/src/mocks/handlers/articles.ts @@ -39,9 +39,11 @@ export const articlesHandler = [ rest.get(`${BASE_URL}/articles/filter`, (req, res, ctx) => { const course = req.url.searchParams.get('course'); - const bookmark = req.url.searchParams.get('bookmark'); + const onlyBookmarked = req.url.searchParams.get('onlyBookmarked'); - const filteredArticle = articles.filter((article) => bookmark === String(article.isBookMarked)); + const filteredArticle = articles.filter( + (article) => onlyBookmarked === String(article.isBookMarked) + ); return res(ctx.status(200), ctx.json(filteredArticle)); }), From 443bfcf8f2ed990a724c7814ef7a79daeaf4aac0 Mon Sep 17 00:00:00 2001 From: hae-on Date: Mon, 18 Sep 2023 15:54:43 +0900 Subject: [PATCH 25/71] =?UTF-8?q?refactor:=20useEffect=EB=A1=9C=20?= =?UTF-8?q?=EB=B6=81=EB=A7=88=ED=81=AC,=20=ED=95=84=ED=84=B0=20=EC=98=B5?= =?UTF-8?q?=EC=85=98=20api=20=EB=B0=9B=EC=95=84=EC=98=A4=EB=8F=84=EB=A1=9D?= =?UTF-8?q?=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: 이도현 --- frontend/src/pages/ArticleListPage/index.tsx | 34 ++++++++++++-------- 1 file changed, 21 insertions(+), 13 deletions(-) diff --git a/frontend/src/pages/ArticleListPage/index.tsx b/frontend/src/pages/ArticleListPage/index.tsx index 2987f7870..bc0ec7f49 100644 --- a/frontend/src/pages/ArticleListPage/index.tsx +++ b/frontend/src/pages/ArticleListPage/index.tsx @@ -8,7 +8,7 @@ import { COLOR, PATH } from '../../constants'; import styled from '@emotion/styled'; import { MainContentStyle } from '../../PageRouter'; import SelectBox from '../../components/Controls/SelectBox'; -import { ChangeEventHandler, useState } from 'react'; +import { useEffect, useState } from 'react'; import { css } from '@emotion/react'; import ArticleBookmarkFilter from '../../components/Article/ArticleBookmarkFIlter'; import { useGetFilteredArticleQuery } from '../../hooks/queries/article'; @@ -35,27 +35,31 @@ const ArticleListPage = () => { const changeFilterOption: (option: { value: string; label: string }) => void = (option) => { setSelectedCourse(option); - // getFilteredArticles(); }; const handleCheckBookmark: React.ChangeEventHandler = (e) => { setChecked(e.currentTarget.checked); - // getFilteredArticles(); }; + useEffect(() => { + getFilteredArticles(); + }, [checked, selectedCourse]); + return (
- - - - + + + + + + + {isLoggedIn && ( + + )}
From b1a13d1270aeb2d90bfaffa6a46b2b670739a304 Mon Sep 17 00:00:00 2001 From: donghae-kim Date: Wed, 20 Sep 2023 17:41:17 +0900 Subject: [PATCH 40/71] =?UTF-8?q?refactor:=20=EC=95=84=ED=8B=B0=ED=81=B4?= =?UTF-8?q?=20=ED=83=80=EC=9E=85=20=EB=B3=80=EA=B2=BD=20=EB=B0=8F=20respon?= =?UTF-8?q?se=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../article/application/ArticleService.java | 15 ++++----------- .../article/domain/ArticleFilterType.java | 17 +++++++++++++++++ .../prolog/article/ui/ArticleController.java | 16 +++++----------- .../prolog/article/ui/ArticleResponse.java | 8 +++++--- .../prolog/common/WebConverterConfig.java | 6 +++--- .../article/application/ArticleServiceTest.java | 6 +++--- 6 files changed, 37 insertions(+), 31 deletions(-) create mode 100644 backend/src/main/java/wooteco/prolog/article/domain/ArticleFilterType.java diff --git a/backend/src/main/java/wooteco/prolog/article/application/ArticleService.java b/backend/src/main/java/wooteco/prolog/article/application/ArticleService.java index 3f7ce7cfe..55ec503ce 100644 --- a/backend/src/main/java/wooteco/prolog/article/application/ArticleService.java +++ b/backend/src/main/java/wooteco/prolog/article/application/ArticleService.java @@ -4,6 +4,7 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import wooteco.prolog.article.domain.Article; +import wooteco.prolog.article.domain.ArticleFilterType; import wooteco.prolog.article.domain.repository.ArticleRepository; import wooteco.prolog.article.ui.ArticleRequest; import wooteco.prolog.article.ui.ArticleResponse; @@ -12,7 +13,6 @@ import wooteco.prolog.login.ui.LoginMember; import wooteco.prolog.member.application.MemberService; import wooteco.prolog.member.domain.Member; -import wooteco.prolog.member.domain.MemberGroupType; import java.util.List; @@ -38,13 +38,6 @@ public Long create(final ArticleRequest articleRequest, final LoginMember loginM return articleRepository.save(article).getId(); } - public List getAll() { - return articleRepository.findAllByOrderByCreatedAtDesc() - .stream() - .map(ArticleResponse::from) - .collect(toList()); - } - @Transactional public void update(final Long id, final ArticleRequest articleRequest, final LoginMember loginMember) { @@ -82,15 +75,15 @@ public void bookmarkArticle(final Long id, final LoginMember loginMember, } } - public List filter(final LoginMember member, final MemberGroupType course, final boolean onlyBookmarked) { + public List getFilteredArticles(final LoginMember member, final ArticleFilterType course, final boolean onlyBookmarked) { if (member.isMember() && onlyBookmarked) { return articleRepository.findArticlesByCourseAndMember(course.getGroupName(), member.getId()).stream() - .map(ArticleResponse::from) + .map(article -> ArticleResponse.of(article,member.getId())) .collect(toList()); } return articleRepository.findArticlesByCourse(course.getGroupName()).stream() - .map(ArticleResponse::from) + .map(article -> ArticleResponse.of(article,member.getId())) .collect(toList()); } } diff --git a/backend/src/main/java/wooteco/prolog/article/domain/ArticleFilterType.java b/backend/src/main/java/wooteco/prolog/article/domain/ArticleFilterType.java new file mode 100644 index 000000000..8eddb65f6 --- /dev/null +++ b/backend/src/main/java/wooteco/prolog/article/domain/ArticleFilterType.java @@ -0,0 +1,17 @@ +package wooteco.prolog.article.domain; + +import lombok.Getter; + +@Getter +public enum ArticleFilterType { + ALL(""), + ANDROID("안드로이드"), + BACKEND("백엔드"), + FRONTEND("프론트엔드"); + + private final String groupName; + + ArticleFilterType(String groupName) { + this.groupName = groupName; + } +} diff --git a/backend/src/main/java/wooteco/prolog/article/ui/ArticleController.java b/backend/src/main/java/wooteco/prolog/article/ui/ArticleController.java index 8c5fa5e26..2ac381fd9 100644 --- a/backend/src/main/java/wooteco/prolog/article/ui/ArticleController.java +++ b/backend/src/main/java/wooteco/prolog/article/ui/ArticleController.java @@ -12,9 +12,9 @@ import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import wooteco.prolog.article.application.ArticleService; +import wooteco.prolog.article.domain.ArticleFilterType; import wooteco.prolog.login.domain.AuthMemberPrincipal; import wooteco.prolog.login.ui.LoginMember; -import wooteco.prolog.member.domain.MemberGroupType; import java.net.URI; import java.util.List; @@ -33,12 +33,6 @@ public ResponseEntity createArticles(@RequestBody final ArticleRequest art return ResponseEntity.created(URI.create("/articles/" + id)).build(); } - @GetMapping - public ResponseEntity> getArticles() { - final List allArticles = articleService.getAll(); - return ResponseEntity.ok(allArticles); - } - @PutMapping("/{id}") public ResponseEntity updateArticle(@RequestBody final ArticleRequest articleRequest, @AuthMemberPrincipal final LoginMember member, @@ -62,11 +56,11 @@ public ResponseEntity bookmarkArticle(@PathVariable final Long id, return ResponseEntity.ok().build(); } - @GetMapping("/filter") - public ResponseEntity> filterArticles(@AuthMemberPrincipal final LoginMember member, - @RequestParam("course") final MemberGroupType course, + @GetMapping + public ResponseEntity> getFilteredArticles(@AuthMemberPrincipal final LoginMember member, + @RequestParam("course") final ArticleFilterType course, @RequestParam("onlyBookmarked") boolean onlyBookmarked) { - final List articleResponses = articleService.filter(member, course, onlyBookmarked); + final List articleResponses = articleService.getFilteredArticles(member, course, onlyBookmarked); return ResponseEntity.ok(articleResponses); } diff --git a/backend/src/main/java/wooteco/prolog/article/ui/ArticleResponse.java b/backend/src/main/java/wooteco/prolog/article/ui/ArticleResponse.java index 6d79c7943..19d398ca5 100644 --- a/backend/src/main/java/wooteco/prolog/article/ui/ArticleResponse.java +++ b/backend/src/main/java/wooteco/prolog/article/ui/ArticleResponse.java @@ -16,20 +16,22 @@ public class ArticleResponse { private final String title; private final String url; private final String imageUrl; + private final boolean isBookmarked; @JsonFormat(pattern = "yyyy-MM-dd HH:mm") private final LocalDateTime createdAt; private ArticleResponse() { - this(null, null, null, null, null, null); + this(null, null, null, null, null, false, null); } - public static ArticleResponse from(final Article article) { + public static ArticleResponse of(final Article article, final Long memberId) { final Long id = article.getId(); final String nickName = article.getMember().getNickname(); final String title = article.getTitle().getTitle(); final String url = article.getUrl().getUrl(); final String imageUrl = article.getImageUrl().getUrl(); + final boolean isBookmarked = article.getArticleBookmarks().containBookmark(memberId); final LocalDateTime createdAt = article.getCreatedAt(); - return new ArticleResponse(id, nickName, title, url, imageUrl, createdAt); + return new ArticleResponse(id, nickName, title, url, imageUrl, isBookmarked, createdAt); } } diff --git a/backend/src/main/java/wooteco/prolog/common/WebConverterConfig.java b/backend/src/main/java/wooteco/prolog/common/WebConverterConfig.java index d7d157281..c0277be27 100644 --- a/backend/src/main/java/wooteco/prolog/common/WebConverterConfig.java +++ b/backend/src/main/java/wooteco/prolog/common/WebConverterConfig.java @@ -3,7 +3,7 @@ import org.springframework.context.annotation.Configuration; import org.springframework.format.FormatterRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; -import wooteco.prolog.member.domain.MemberGroupType; +import wooteco.prolog.article.domain.ArticleFilterType; import java.time.LocalDate; import java.time.Month; @@ -21,8 +21,8 @@ public void addFormatters(FormatterRegistry registry) { source -> LocalDate.parse(source, DateTimeFormatter.BASIC_ISO_DATE) ); - registry.addConverter(String.class, MemberGroupType.class, - source -> MemberGroupType.valueOf(source.toUpperCase()) + registry.addConverter(String.class, ArticleFilterType.class, + source -> ArticleFilterType.valueOf(source.toUpperCase()) ); } } diff --git a/backend/src/test/java/wooteco/prolog/article/application/ArticleServiceTest.java b/backend/src/test/java/wooteco/prolog/article/application/ArticleServiceTest.java index 00b94cb45..1d5c4a366 100644 --- a/backend/src/test/java/wooteco/prolog/article/application/ArticleServiceTest.java +++ b/backend/src/test/java/wooteco/prolog/article/application/ArticleServiceTest.java @@ -10,6 +10,7 @@ import org.mockito.junit.jupiter.MockitoExtension; import wooteco.prolog.article.domain.Article; import wooteco.prolog.article.domain.ArticleBookmarks; +import wooteco.prolog.article.domain.ArticleFilterType; import wooteco.prolog.article.domain.ImageUrl; import wooteco.prolog.article.domain.Title; import wooteco.prolog.article.domain.Url; @@ -20,7 +21,6 @@ import wooteco.prolog.login.ui.LoginMember; import wooteco.prolog.member.application.MemberService; import wooteco.prolog.member.domain.Member; -import wooteco.prolog.member.domain.MemberGroupType; import wooteco.prolog.member.domain.Role; import java.util.Arrays; @@ -251,7 +251,7 @@ void filter() { when(articleRepository.findArticlesByCourse(any())).thenReturn(Arrays.asList(article)); //when - final List articleResponses = articleService.filter(unLoginMember, MemberGroupType.BACKEND, false); + final List articleResponses = articleService.getFilteredArticles(unLoginMember, ArticleFilterType.BACKEND, false); //then verify(articleRepository).findArticlesByCourse(any()); @@ -269,7 +269,7 @@ void filter_isBookmarked() { when(articleRepository.findArticlesByCourseAndMember(any(), any())).thenReturn(Arrays.asList(article)); //when - final List articleResponses = articleService.filter(loginMember, MemberGroupType.BACKEND, true); + final List articleResponses = articleService.getFilteredArticles(loginMember, ArticleFilterType.BACKEND, true); //then verify(articleRepository).findArticlesByCourseAndMember(any(), any()); From d9501c1c4b430581cfc514aea0552043f449523b Mon Sep 17 00:00:00 2001 From: hae-on Date: Wed, 20 Sep 2023 17:43:19 +0900 Subject: [PATCH 41/71] =?UTF-8?q?refactor:=20msw=20filtering=20=EA=B8=B0?= =?UTF-8?q?=EB=8A=A5=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/mocks/db/articles-android.json | 29 +++++++++++ frontend/src/mocks/db/articles-backend.json | 29 +++++++++++ frontend/src/mocks/db/articles-frontend.json | 29 +++++++++++ frontend/src/mocks/db/articles.json | 51 ++++++++++++++++++-- frontend/src/mocks/handlers/articles.ts | 32 ++++++++---- 5 files changed, 158 insertions(+), 12 deletions(-) create mode 100644 frontend/src/mocks/db/articles-android.json create mode 100644 frontend/src/mocks/db/articles-backend.json create mode 100644 frontend/src/mocks/db/articles-frontend.json diff --git a/frontend/src/mocks/db/articles-android.json b/frontend/src/mocks/db/articles-android.json new file mode 100644 index 000000000..865954a4c --- /dev/null +++ b/frontend/src/mocks/db/articles-android.json @@ -0,0 +1,29 @@ +[ + { + "id": 7, + "userName": "도밥", + "title": "직렬화, 역직렬화는 무엇일까?", + "url": "https://think0wise.tistory.com/107", + "createdAt": "2023-07-08 16:48", + "isBookMarked": false, + "imageUrl": "https://plus.unsplash.com/premium_photo-1682088845396-1b310a002302?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8MXx8JUVDJTk1JTg0JUVEJThCJUIwJUVEJTgxJUI0fGVufDB8fDB8fHww&auto=format&fit=crop&w=500&q=60" + }, + { + "id": 8, + "userName": "도밥", + "title": "직렬화, 역직렬화는 무엇일까?", + "url": "https://think0wise.tistory.com/107", + "createdAt": "2023-07-08 16:48", + "isBookMarked": true, + "imageUrl": "https://plus.unsplash.com/premium_photo-1682088845396-1b310a002302?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8MXx8JUVDJTk1JTg0JUVEJThCJUIwJUVEJTgxJUI0fGVufDB8fDB8fHww&auto=format&fit=crop&w=500&q=60" + }, + { + "id": 9, + "userName": "도밥", + "title": "직렬화, 역직렬화는 무엇일까?", + "url": "https://think0wise.tistory.com/107", + "createdAt": "2023-07-08 16:48", + "isBookMarked": false, + "imageUrl": "https://plus.unsplash.com/premium_photo-1682088845396-1b310a002302?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8MXx8JUVDJTk1JTg0JUVEJThCJUIwJUVEJTgxJUI0fGVufDB8fDB8fHww&auto=format&fit=crop&w=500&q=60" + } +] diff --git a/frontend/src/mocks/db/articles-backend.json b/frontend/src/mocks/db/articles-backend.json new file mode 100644 index 000000000..f3fdaae4c --- /dev/null +++ b/frontend/src/mocks/db/articles-backend.json @@ -0,0 +1,29 @@ +[ + { + "id": 4, + "userName": "패트릭", + "title": "CORS", + "isBookMarked": false, + "url": "https://pgccoding.tistory.com/66", + "createdAt": "2023-07-24 18:18", + "imageUrl": "https://plus.unsplash.com/premium_photo-1682088845396-1b310a002302?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8MXx8JUVDJTk1JTg0JUVEJThCJUIwJUVEJTgxJUI0fGVufDB8fDB8fHww&auto=format&fit=crop&w=500&q=60" + }, + { + "id": 5, + "userName": "패트릭", + "title": "CORS", + "isBookMarked": true, + "url": "https://pgccoding.tistory.com/66", + "createdAt": "2023-07-24 18:18", + "imageUrl": "https://plus.unsplash.com/premium_photo-1682088845396-1b310a002302?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8MXx8JUVDJTk1JTg0JUVEJThCJUIwJUVEJTgxJUI0fGVufDB8fDB8fHww&auto=format&fit=crop&w=500&q=60" + }, + { + "id": 6, + "userName": "패트릭", + "title": "CORS", + "isBookMarked": true, + "url": "https://pgccoding.tistory.com/66", + "createdAt": "2023-07-24 18:18", + "imageUrl": "https://plus.unsplash.com/premium_photo-1682088845396-1b310a002302?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8MXx8JUVDJTk1JTg0JUVEJThCJUIwJUVEJTgxJUI0fGVufDB8fDB8fHww&auto=format&fit=crop&w=500&q=60" + } +] diff --git a/frontend/src/mocks/db/articles-frontend.json b/frontend/src/mocks/db/articles-frontend.json new file mode 100644 index 000000000..d7da0c5e8 --- /dev/null +++ b/frontend/src/mocks/db/articles-frontend.json @@ -0,0 +1,29 @@ +[ + { + "id": 1, + "userName": "해온", + "title": "Axios", + "isBookMarked": true, + "url": "https://hae-on.tistory.com/104", + "createdAt": "2023-07-24 18:18", + "imageUrl": "https://plus.unsplash.com/premium_photo-1682088845396-1b310a002302?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8MXx8JUVDJTk1JTg0JUVEJThCJUIwJUVEJTgxJUI0fGVufDB8fDB8fHww&auto=format&fit=crop&w=500&q=60" + }, + { + "id": 2, + "userName": "해온", + "title": "Axios", + "isBookMarked": true, + "url": "https://hae-on.tistory.com/104", + "createdAt": "2023-07-24 18:18", + "imageUrl": "https://plus.unsplash.com/premium_photo-1682088845396-1b310a002302?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8MXx8JUVDJTk1JTg0JUVEJThCJUIwJUVEJTgxJUI0fGVufDB8fDB8fHww&auto=format&fit=crop&w=500&q=60" + }, + { + "id": 3, + "userName": "해온", + "title": "Axios", + "isBookMarked": true, + "url": "https://hae-on.tistory.com/104", + "createdAt": "2023-07-24 18:18", + "imageUrl": "https://plus.unsplash.com/premium_photo-1682088845396-1b310a002302?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8MXx8JUVDJTk1JTg0JUVEJThCJUIwJUVEJTgxJUI0fGVufDB8fDB8fHww&auto=format&fit=crop&w=500&q=60" + } +] diff --git a/frontend/src/mocks/db/articles.json b/frontend/src/mocks/db/articles.json index 182d5e558..35b46c204 100644 --- a/frontend/src/mocks/db/articles.json +++ b/frontend/src/mocks/db/articles.json @@ -10,6 +10,24 @@ }, { "id": 2, + "userName": "해온", + "title": "Axios", + "isBookMarked": true, + "url": "https://hae-on.tistory.com/104", + "createdAt": "2023-07-24 18:18", + "imageUrl": "https://plus.unsplash.com/premium_photo-1682088845396-1b310a002302?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8MXx8JUVDJTk1JTg0JUVEJThCJUIwJUVEJTgxJUI0fGVufDB8fDB8fHww&auto=format&fit=crop&w=500&q=60" + }, + { + "id": 3, + "userName": "해온", + "title": "Axios", + "isBookMarked": true, + "url": "https://hae-on.tistory.com/104", + "createdAt": "2023-07-24 18:18", + "imageUrl": "https://plus.unsplash.com/premium_photo-1682088845396-1b310a002302?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8MXx8JUVDJTk1JTg0JUVEJThCJUIwJUVEJTgxJUI0fGVufDB8fDB8fHww&auto=format&fit=crop&w=500&q=60" + }, + { + "id": 4, "userName": "패트릭", "title": "CORS", "isBookMarked": false, @@ -18,7 +36,7 @@ "imageUrl": "https://plus.unsplash.com/premium_photo-1682088845396-1b310a002302?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8MXx8JUVDJTk1JTg0JUVEJThCJUIwJUVEJTgxJUI0fGVufDB8fDB8fHww&auto=format&fit=crop&w=500&q=60" }, { - "id": 3, + "id": 5, "userName": "패트릭", "title": "CORS", "isBookMarked": true, @@ -27,12 +45,39 @@ "imageUrl": "https://plus.unsplash.com/premium_photo-1682088845396-1b310a002302?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8MXx8JUVDJTk1JTg0JUVEJThCJUIwJUVEJTgxJUI0fGVufDB8fDB8fHww&auto=format&fit=crop&w=500&q=60" }, { - "id": 4, + "id": 6, "userName": "패트릭", "title": "CORS", - "isBookMarked": false, + "isBookMarked": true, "url": "https://pgccoding.tistory.com/66", "createdAt": "2023-07-24 18:18", "imageUrl": "https://plus.unsplash.com/premium_photo-1682088845396-1b310a002302?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8MXx8JUVDJTk1JTg0JUVEJThCJUIwJUVEJTgxJUI0fGVufDB8fDB8fHww&auto=format&fit=crop&w=500&q=60" + }, + { + "id": 7, + "userName": "도밥", + "title": "직렬화, 역직렬화는 무엇일까?", + "url": "https://think0wise.tistory.com/107", + "createdAt": "2023-07-08 16:48", + "isBookMarked": false, + "imageUrl": "https://plus.unsplash.com/premium_photo-1682088845396-1b310a002302?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8MXx8JUVDJTk1JTg0JUVEJThCJUIwJUVEJTgxJUI0fGVufDB8fDB8fHww&auto=format&fit=crop&w=500&q=60" + }, + { + "id": 8, + "userName": "도밥", + "title": "직렬화, 역직렬화는 무엇일까?", + "url": "https://think0wise.tistory.com/107", + "createdAt": "2023-07-08 16:48", + "isBookMarked": true, + "imageUrl": "https://plus.unsplash.com/premium_photo-1682088845396-1b310a002302?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8MXx8JUVDJTk1JTg0JUVEJThCJUIwJUVEJTgxJUI0fGVufDB8fDB8fHww&auto=format&fit=crop&w=500&q=60" + }, + { + "id": 9, + "userName": "도밥", + "title": "직렬화, 역직렬화는 무엇일까?", + "url": "https://think0wise.tistory.com/107", + "createdAt": "2023-07-08 16:48", + "isBookMarked": false, + "imageUrl": "https://plus.unsplash.com/premium_photo-1682088845396-1b310a002302?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8MXx8JUVDJTk1JTg0JUVEJThCJUIwJUVEJTgxJUI0fGVufDB8fDB8fHww&auto=format&fit=crop&w=500&q=60" } ] diff --git a/frontend/src/mocks/handlers/articles.ts b/frontend/src/mocks/handlers/articles.ts index 2302996cc..6571fa4fa 100644 --- a/frontend/src/mocks/handlers/articles.ts +++ b/frontend/src/mocks/handlers/articles.ts @@ -1,15 +1,18 @@ import { rest } from 'msw'; import { BASE_URL } from '../../configs/environment'; import articles from '../db/articles.json'; +import articlesFrontend from '../db/articles-frontend.json'; +import articlesBackend from '../db/articles-backend.json'; +import articlesAndroid from '../db/articles-android.json'; import metaOg from '../db/metaog.json'; import { ArticleType } from '../../models/Article'; const articleUrl = 'https://think0wise.tistory.com/107'; export const articlesHandler = [ - rest.get(`${BASE_URL}/articles`, (req, res, ctx) => { - return res(ctx.status(200), ctx.json(articles)); - }), + // rest.get(`${BASE_URL}/articles`, (req, res, ctx) => { + // return res(ctx.status(200), ctx.json(articles)); + // }), rest.get(`${BASE_URL}/meta-og?url=${articleUrl}`, async (req, res, ctx) => { const data = metaOg; @@ -37,13 +40,24 @@ export const articlesHandler = [ return res(ctx.status(200)); }), - rest.get(`${BASE_URL}/articles/`, (req, res, ctx) => { - const course = req.url.searchParams.get('course'); - const onlyBookmarked = req.url.searchParams.get('onlyBookmarked'); + rest.get(`${BASE_URL}/articles`, (req, res, ctx) => { + const course = req.url.searchParams.get('course') ?? 'all'; + const onlyBookmarked = req.url.searchParams.get('onlyBookmarked') as string; + + const filteredCourse = (course: string) => { + if (course === 'all') return articles; + if (course === 'frontend') return articlesFrontend; + if (course === 'backend') return articlesBackend; + if (course === 'android') return articlesAndroid; + }; + + if (onlyBookmarked === 'false') { + return res(ctx.status(200), ctx.json(filteredCourse(course))); + } - const filteredArticle = articles.filter( - (article) => onlyBookmarked === String(article.isBookMarked) - ); + const filteredArticle = filteredCourse(course)?.filter((article) => { + return onlyBookmarked === String(article.isBookMarked) ? true : false; + }); return res(ctx.status(200), ctx.json(filteredArticle)); }), From 3e563aa991d471a973a01bc2f6be080cd37d862e Mon Sep 17 00:00:00 2001 From: hong-sile Date: Mon, 25 Sep 2023 15:43:45 +0900 Subject: [PATCH 42/71] =?UTF-8?q?feat:=20=EC=95=84=ED=8B=B0=ED=81=B4?= =?UTF-8?q?=EC=9D=84=20=EC=A1=B0=ED=9A=8C=ED=95=A0=20=EB=95=8C=20like?= =?UTF-8?q?=EB=A5=BC=20fetch=EB=A1=9C=20=EC=A1=B0=ED=9A=8C=ED=95=98?= =?UTF-8?q?=EB=8A=94=20=EA=B8=B0=EB=8A=A5=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../prolog/article/domain/repository/ArticleRepository.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/backend/src/main/java/wooteco/prolog/article/domain/repository/ArticleRepository.java b/backend/src/main/java/wooteco/prolog/article/domain/repository/ArticleRepository.java index df5efe865..0a6780b1b 100644 --- a/backend/src/main/java/wooteco/prolog/article/domain/repository/ArticleRepository.java +++ b/backend/src/main/java/wooteco/prolog/article/domain/repository/ArticleRepository.java @@ -12,5 +12,8 @@ public interface ArticleRepository extends JpaRepository { List
findAllByOrderByCreatedAtDesc(); @Query("select a from Article a join fetch a.articleBookmarks where a.id = :id") - Optional
findFetchById(@Param("id") final Long id); + Optional
findFetchBookmarkById(@Param("id") final Long id); + + @Query("select a from Article a join fetch a.articleLikes where a.id = :id") + Optional
findFetchLikeById(@Param("id") final Long id); } From 41d5a504c6c3e4f974a2e23f0b65805115d8997b Mon Sep 17 00:00:00 2001 From: hong-sile Date: Mon, 25 Sep 2023 15:44:20 +0900 Subject: [PATCH 43/71] =?UTF-8?q?feat:=20flyway=20schema=20foreign=20key?= =?UTF-8?q?=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../db/migration/prod/V6__create_table_article_bookmark.sql | 4 +++- .../db/migration/prod/V7__create_table_article_like.sql | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/backend/src/main/resources/db/migration/prod/V6__create_table_article_bookmark.sql b/backend/src/main/resources/db/migration/prod/V6__create_table_article_bookmark.sql index fc5b4f786..20062f012 100644 --- a/backend/src/main/resources/db/migration/prod/V6__create_table_article_bookmark.sql +++ b/backend/src/main/resources/db/migration/prod/V6__create_table_article_bookmark.sql @@ -2,7 +2,9 @@ create table if not exists prolog.article_bookmark ( id bigint auto_increment primary key, article_id bigint not null, - member_id bigint not null + member_id bigint not null, + foreign key (member_id) references prolog.member (id), + foreign key (article_id) references prolog.article (id) ) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci; diff --git a/backend/src/main/resources/db/migration/prod/V7__create_table_article_like.sql b/backend/src/main/resources/db/migration/prod/V7__create_table_article_like.sql index 4fbe72b0e..59700e066 100644 --- a/backend/src/main/resources/db/migration/prod/V7__create_table_article_like.sql +++ b/backend/src/main/resources/db/migration/prod/V7__create_table_article_like.sql @@ -2,7 +2,9 @@ create table if not exists prolog.article_like ( id bigint auto_increment primary key, article_id bigint not null, - member_id bigint not null + member_id bigint not null, + foreign key (member_id) references prolog.member (id), + foreign key (article_id) references prolog.article (id) ) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci; From 0eab097d05a3e19a7d0df75483b780407e334f52 Mon Sep 17 00:00:00 2001 From: hong-sile Date: Mon, 25 Sep 2023 15:44:58 +0900 Subject: [PATCH 44/71] =?UTF-8?q?refactor:=20=ED=94=84=EB=A1=A0=ED=8A=B8?= =?UTF-8?q?=EC=99=80=20=EB=85=BC=EC=9D=98=20=ED=9B=84=20API=20=EB=AA=85?= =?UTF-8?q?=EC=84=B8=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/wooteco/prolog/docu/ArticleDocumentation.java | 2 +- .../java/wooteco/prolog/article/ui/ArticleController.java | 4 ++-- .../java/wooteco/prolog/article/ui/ArticleLikesRequest.java | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/backend/src/documentation/java/wooteco/prolog/docu/ArticleDocumentation.java b/backend/src/documentation/java/wooteco/prolog/docu/ArticleDocumentation.java index 43832e60e..2bab040a4 100644 --- a/backend/src/documentation/java/wooteco/prolog/docu/ArticleDocumentation.java +++ b/backend/src/documentation/java/wooteco/prolog/docu/ArticleDocumentation.java @@ -55,6 +55,6 @@ public class ArticleDocumentation extends NewDocumentation { response.expect(status().isOk()); //docs - response.apply(document("article/likes")); + response.apply(document("article/like")); } } diff --git a/backend/src/main/java/wooteco/prolog/article/ui/ArticleController.java b/backend/src/main/java/wooteco/prolog/article/ui/ArticleController.java index 9bf7d2171..661fc312f 100644 --- a/backend/src/main/java/wooteco/prolog/article/ui/ArticleController.java +++ b/backend/src/main/java/wooteco/prolog/article/ui/ArticleController.java @@ -59,11 +59,11 @@ public ResponseEntity bookmarkArticle(@PathVariable final Long id, return ResponseEntity.ok().build(); } - @PutMapping("/{id}/likes") + @PutMapping("/{id}/like") public ResponseEntity likeArticle(@PathVariable final Long id, @AuthMemberPrincipal final LoginMember member, @RequestBody final ArticleLikesRequest request) { - articleService.likeArticle(id, member, request.getLikes()); + articleService.likeArticle(id, member, request.getLike()); return ResponseEntity.ok().build(); } } diff --git a/backend/src/main/java/wooteco/prolog/article/ui/ArticleLikesRequest.java b/backend/src/main/java/wooteco/prolog/article/ui/ArticleLikesRequest.java index e1e4a9b89..9dce50790 100644 --- a/backend/src/main/java/wooteco/prolog/article/ui/ArticleLikesRequest.java +++ b/backend/src/main/java/wooteco/prolog/article/ui/ArticleLikesRequest.java @@ -7,7 +7,7 @@ @RequiredArgsConstructor public class ArticleLikesRequest { - private final Boolean likes; + private final Boolean like; public ArticleLikesRequest() { this(null); From fc90474e140993d4d189d7a09099e60c047edcf7 Mon Sep 17 00:00:00 2001 From: hae-on Date: Mon, 25 Sep 2023 15:56:00 +0900 Subject: [PATCH 45/71] =?UTF-8?q?feat:=20=EC=A2=8B=EC=95=84=EC=9A=94=20?= =?UTF-8?q?=EA=B8=B0=EB=8A=A5=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: 이도현 --- frontend/src/apis/articles.ts | 7 +++- .../src/components/Article/Article.style.tsx | 3 +- frontend/src/components/Article/Article.tsx | 36 ++++++++++++++++--- frontend/src/components/Reaction/Like.tsx | 4 +-- frontend/src/hooks/queries/article.ts | 10 +++++- frontend/src/mocks/db/articles-android.json | 3 ++ frontend/src/mocks/db/articles-backend.json | 3 ++ frontend/src/mocks/db/articles-frontend.json | 3 ++ frontend/src/mocks/db/articles.json | 9 +++++ frontend/src/mocks/handlers/articles.ts | 9 ++--- frontend/src/models/Article.ts | 6 ++++ 11 files changed, 80 insertions(+), 13 deletions(-) diff --git a/frontend/src/apis/articles.ts b/frontend/src/apis/articles.ts index e3f223ea0..720fc2fbc 100644 --- a/frontend/src/apis/articles.ts +++ b/frontend/src/apis/articles.ts @@ -1,6 +1,7 @@ import { client } from '.'; import { ArticleBookmarkPutRequest, + ArticleLikePutRequest, ArticleRequest, MetaOgRequest, MetaOgResponse, @@ -15,9 +16,13 @@ export const requestGetMetaOg = ({ url }: MetaOgRequest) => { }; export const requestPutArticleBookmark = ({ articleId, bookmark }: ArticleBookmarkPutRequest) => { - return client.put(`/articles/${articleId}/bookmark`, { checked: bookmark }); + return client.put(`/articles/${articleId}/bookmark`, { bookmark }); }; export const requestGetFilteredArticle = (course: string, bookmark: boolean) => { return client.get(`/articles?course=${course}&onlyBookmarked=${bookmark}`); }; + +export const requestPutArticleLike = ({ articleId, like }: ArticleLikePutRequest) => { + return client.put(`/articles/${articleId}/like`, { like }); +}; diff --git a/frontend/src/components/Article/Article.style.tsx b/frontend/src/components/Article/Article.style.tsx index 13980b9b4..e4a68ba44 100644 --- a/frontend/src/components/Article/Article.style.tsx +++ b/frontend/src/components/Article/Article.style.tsx @@ -72,10 +72,11 @@ export const Title = styled.p` -webkit-box-orient: vertical; `; -export const BookmarkWrapper = styled.div` +export const ButtonContainer = styled.div` width: 100%; display: flex; justify-content: flex-end; + gap: 10px; `; export const ArticleBookmarkButtonStyle = css` diff --git a/frontend/src/components/Article/Article.tsx b/frontend/src/components/Article/Article.tsx index 0d55d5f71..5632e91fa 100644 --- a/frontend/src/components/Article/Article.tsx +++ b/frontend/src/components/Article/Article.tsx @@ -2,13 +2,29 @@ import * as Styled from './Article.style'; import type { ArticleType } from '../../models/Article'; import Scrap from '../Reaction/Scrap'; import { useRef, useState } from 'react'; -import { usePutArticleBookmarkMutation } from '../../hooks/queries/article'; +import { + usePutArticleBookmarkMutation, + usePutArticleLikeMutation, +} from '../../hooks/queries/article'; import debounce from '../../utils/debounce'; +import Like from '../Reaction/Like'; -const Article = ({ id, title, userName, url, createdAt, imageUrl, isBookMarked }: ArticleType) => { +const Article = ({ + id, + title, + userName, + url, + createdAt, + imageUrl, + isBookMarked, + isLiked, +}: ArticleType) => { const bookmarkRef = useRef(false); + const likeRef = useRef(false); const [bookmark, setBookmark] = useState(isBookMarked); + const [like, setLike] = useState(isLiked); const { mutate: putBookmark } = usePutArticleBookmarkMutation(); + const { mutate: putLike } = usePutArticleLikeMutation(); const toggleBookmark: React.MouseEventHandler = (e) => { e.preventDefault(); @@ -21,6 +37,17 @@ const Article = ({ id, title, userName, url, createdAt, imageUrl, isBookMarked } }, 300); }; + const toggleLike: React.MouseEventHandler = (e) => { + e.preventDefault(); + + likeRef.current = !likeRef.current; + setLike((prev) => !prev); + + debounce(() => { + putLike({ articleId: id, like: likeRef.current }); + }, 300); + }; + return ( @@ -33,13 +60,14 @@ const Article = ({ id, title, userName, url, createdAt, imageUrl, isBookMarked } {createdAt.split(' ')[0]} {title} - + + - + diff --git a/frontend/src/components/Reaction/Like.tsx b/frontend/src/components/Reaction/Like.tsx index 555bbbd4f..8674733eb 100644 --- a/frontend/src/components/Reaction/Like.tsx +++ b/frontend/src/components/Reaction/Like.tsx @@ -7,7 +7,7 @@ import { LikeIconStyle } from './Like.styles'; interface Props { liked: boolean; - likesCount: number; + likesCount?: number; onClick: MouseEventHandler; } @@ -24,7 +24,7 @@ const Like = ({ liked, likesCount, onClick }: Props) => { cssProps={LikeIconStyle} onClick={onClick} > - {likesCount ?? 0} + {likesCount} ); }; diff --git a/frontend/src/hooks/queries/article.ts b/frontend/src/hooks/queries/article.ts index f62cd49f8..59caec13a 100644 --- a/frontend/src/hooks/queries/article.ts +++ b/frontend/src/hooks/queries/article.ts @@ -3,8 +3,9 @@ import { requestGetFilteredArticle, requestPostArticles, requestPutArticleBookmark, + requestPutArticleLike, } from '../../apis/articles'; -import { ArticleType, CourseFilter } from '../../models/Article'; +import { ArticleType } from '../../models/Article'; import { ERROR_MESSAGE } from '../../constants'; import { SUCCESS_MESSAGE } from '../../constants/message'; @@ -40,3 +41,10 @@ export const usePutArticleBookmarkMutation = () => { onError: () => {}, }); }; + +export const usePutArticleLikeMutation = () => { + return useMutation(requestPutArticleLike, { + onSuccess: () => {}, + onError: () => {}, + }); +}; diff --git a/frontend/src/mocks/db/articles-android.json b/frontend/src/mocks/db/articles-android.json index 865954a4c..0405b7a1e 100644 --- a/frontend/src/mocks/db/articles-android.json +++ b/frontend/src/mocks/db/articles-android.json @@ -5,6 +5,7 @@ "title": "직렬화, 역직렬화는 무엇일까?", "url": "https://think0wise.tistory.com/107", "createdAt": "2023-07-08 16:48", + "isLiked": false, "isBookMarked": false, "imageUrl": "https://plus.unsplash.com/premium_photo-1682088845396-1b310a002302?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8MXx8JUVDJTk1JTg0JUVEJThCJUIwJUVEJTgxJUI0fGVufDB8fDB8fHww&auto=format&fit=crop&w=500&q=60" }, @@ -14,6 +15,7 @@ "title": "직렬화, 역직렬화는 무엇일까?", "url": "https://think0wise.tistory.com/107", "createdAt": "2023-07-08 16:48", + "isLiked": false, "isBookMarked": true, "imageUrl": "https://plus.unsplash.com/premium_photo-1682088845396-1b310a002302?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8MXx8JUVDJTk1JTg0JUVEJThCJUIwJUVEJTgxJUI0fGVufDB8fDB8fHww&auto=format&fit=crop&w=500&q=60" }, @@ -23,6 +25,7 @@ "title": "직렬화, 역직렬화는 무엇일까?", "url": "https://think0wise.tistory.com/107", "createdAt": "2023-07-08 16:48", + "isLiked": false, "isBookMarked": false, "imageUrl": "https://plus.unsplash.com/premium_photo-1682088845396-1b310a002302?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8MXx8JUVDJTk1JTg0JUVEJThCJUIwJUVEJTgxJUI0fGVufDB8fDB8fHww&auto=format&fit=crop&w=500&q=60" } diff --git a/frontend/src/mocks/db/articles-backend.json b/frontend/src/mocks/db/articles-backend.json index f3fdaae4c..9f5051d5a 100644 --- a/frontend/src/mocks/db/articles-backend.json +++ b/frontend/src/mocks/db/articles-backend.json @@ -4,6 +4,7 @@ "userName": "패트릭", "title": "CORS", "isBookMarked": false, + "isLiked": false, "url": "https://pgccoding.tistory.com/66", "createdAt": "2023-07-24 18:18", "imageUrl": "https://plus.unsplash.com/premium_photo-1682088845396-1b310a002302?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8MXx8JUVDJTk1JTg0JUVEJThCJUIwJUVEJTgxJUI0fGVufDB8fDB8fHww&auto=format&fit=crop&w=500&q=60" @@ -13,6 +14,7 @@ "userName": "패트릭", "title": "CORS", "isBookMarked": true, + "isLiked": false, "url": "https://pgccoding.tistory.com/66", "createdAt": "2023-07-24 18:18", "imageUrl": "https://plus.unsplash.com/premium_photo-1682088845396-1b310a002302?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8MXx8JUVDJTk1JTg0JUVEJThCJUIwJUVEJTgxJUI0fGVufDB8fDB8fHww&auto=format&fit=crop&w=500&q=60" @@ -22,6 +24,7 @@ "userName": "패트릭", "title": "CORS", "isBookMarked": true, + "isLiked": false, "url": "https://pgccoding.tistory.com/66", "createdAt": "2023-07-24 18:18", "imageUrl": "https://plus.unsplash.com/premium_photo-1682088845396-1b310a002302?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8MXx8JUVDJTk1JTg0JUVEJThCJUIwJUVEJTgxJUI0fGVufDB8fDB8fHww&auto=format&fit=crop&w=500&q=60" diff --git a/frontend/src/mocks/db/articles-frontend.json b/frontend/src/mocks/db/articles-frontend.json index d7da0c5e8..bf065cdbf 100644 --- a/frontend/src/mocks/db/articles-frontend.json +++ b/frontend/src/mocks/db/articles-frontend.json @@ -4,6 +4,7 @@ "userName": "해온", "title": "Axios", "isBookMarked": true, + "isLiked": false, "url": "https://hae-on.tistory.com/104", "createdAt": "2023-07-24 18:18", "imageUrl": "https://plus.unsplash.com/premium_photo-1682088845396-1b310a002302?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8MXx8JUVDJTk1JTg0JUVEJThCJUIwJUVEJTgxJUI0fGVufDB8fDB8fHww&auto=format&fit=crop&w=500&q=60" @@ -13,6 +14,7 @@ "userName": "해온", "title": "Axios", "isBookMarked": true, + "isLiked": false, "url": "https://hae-on.tistory.com/104", "createdAt": "2023-07-24 18:18", "imageUrl": "https://plus.unsplash.com/premium_photo-1682088845396-1b310a002302?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8MXx8JUVDJTk1JTg0JUVEJThCJUIwJUVEJTgxJUI0fGVufDB8fDB8fHww&auto=format&fit=crop&w=500&q=60" @@ -22,6 +24,7 @@ "userName": "해온", "title": "Axios", "isBookMarked": true, + "isLiked": false, "url": "https://hae-on.tistory.com/104", "createdAt": "2023-07-24 18:18", "imageUrl": "https://plus.unsplash.com/premium_photo-1682088845396-1b310a002302?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8MXx8JUVDJTk1JTg0JUVEJThCJUIwJUVEJTgxJUI0fGVufDB8fDB8fHww&auto=format&fit=crop&w=500&q=60" diff --git a/frontend/src/mocks/db/articles.json b/frontend/src/mocks/db/articles.json index 35b46c204..246762fb4 100644 --- a/frontend/src/mocks/db/articles.json +++ b/frontend/src/mocks/db/articles.json @@ -4,6 +4,7 @@ "userName": "해온", "title": "Axios", "isBookMarked": true, + "isLiked": false, "url": "https://hae-on.tistory.com/104", "createdAt": "2023-07-24 18:18", "imageUrl": "https://plus.unsplash.com/premium_photo-1682088845396-1b310a002302?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8MXx8JUVDJTk1JTg0JUVEJThCJUIwJUVEJTgxJUI0fGVufDB8fDB8fHww&auto=format&fit=crop&w=500&q=60" @@ -13,6 +14,7 @@ "userName": "해온", "title": "Axios", "isBookMarked": true, + "isLiked": false, "url": "https://hae-on.tistory.com/104", "createdAt": "2023-07-24 18:18", "imageUrl": "https://plus.unsplash.com/premium_photo-1682088845396-1b310a002302?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8MXx8JUVDJTk1JTg0JUVEJThCJUIwJUVEJTgxJUI0fGVufDB8fDB8fHww&auto=format&fit=crop&w=500&q=60" @@ -22,6 +24,7 @@ "userName": "해온", "title": "Axios", "isBookMarked": true, + "isLiked": false, "url": "https://hae-on.tistory.com/104", "createdAt": "2023-07-24 18:18", "imageUrl": "https://plus.unsplash.com/premium_photo-1682088845396-1b310a002302?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8MXx8JUVDJTk1JTg0JUVEJThCJUIwJUVEJTgxJUI0fGVufDB8fDB8fHww&auto=format&fit=crop&w=500&q=60" @@ -31,6 +34,7 @@ "userName": "패트릭", "title": "CORS", "isBookMarked": false, + "isLiked": false, "url": "https://pgccoding.tistory.com/66", "createdAt": "2023-07-24 18:18", "imageUrl": "https://plus.unsplash.com/premium_photo-1682088845396-1b310a002302?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8MXx8JUVDJTk1JTg0JUVEJThCJUIwJUVEJTgxJUI0fGVufDB8fDB8fHww&auto=format&fit=crop&w=500&q=60" @@ -40,6 +44,7 @@ "userName": "패트릭", "title": "CORS", "isBookMarked": true, + "isLiked": false, "url": "https://pgccoding.tistory.com/66", "createdAt": "2023-07-24 18:18", "imageUrl": "https://plus.unsplash.com/premium_photo-1682088845396-1b310a002302?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8MXx8JUVDJTk1JTg0JUVEJThCJUIwJUVEJTgxJUI0fGVufDB8fDB8fHww&auto=format&fit=crop&w=500&q=60" @@ -49,6 +54,7 @@ "userName": "패트릭", "title": "CORS", "isBookMarked": true, + "isLiked": false, "url": "https://pgccoding.tistory.com/66", "createdAt": "2023-07-24 18:18", "imageUrl": "https://plus.unsplash.com/premium_photo-1682088845396-1b310a002302?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8MXx8JUVDJTk1JTg0JUVEJThCJUIwJUVEJTgxJUI0fGVufDB8fDB8fHww&auto=format&fit=crop&w=500&q=60" @@ -60,6 +66,7 @@ "url": "https://think0wise.tistory.com/107", "createdAt": "2023-07-08 16:48", "isBookMarked": false, + "isLiked": false, "imageUrl": "https://plus.unsplash.com/premium_photo-1682088845396-1b310a002302?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8MXx8JUVDJTk1JTg0JUVEJThCJUIwJUVEJTgxJUI0fGVufDB8fDB8fHww&auto=format&fit=crop&w=500&q=60" }, { @@ -69,6 +76,7 @@ "url": "https://think0wise.tistory.com/107", "createdAt": "2023-07-08 16:48", "isBookMarked": true, + "isLiked": false, "imageUrl": "https://plus.unsplash.com/premium_photo-1682088845396-1b310a002302?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8MXx8JUVDJTk1JTg0JUVEJThCJUIwJUVEJTgxJUI0fGVufDB8fDB8fHww&auto=format&fit=crop&w=500&q=60" }, { @@ -78,6 +86,7 @@ "url": "https://think0wise.tistory.com/107", "createdAt": "2023-07-08 16:48", "isBookMarked": false, + "isLiked": false, "imageUrl": "https://plus.unsplash.com/premium_photo-1682088845396-1b310a002302?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8MXx8JUVDJTk1JTg0JUVEJThCJUIwJUVEJTgxJUI0fGVufDB8fDB8fHww&auto=format&fit=crop&w=500&q=60" } ] diff --git a/frontend/src/mocks/handlers/articles.ts b/frontend/src/mocks/handlers/articles.ts index 6571fa4fa..489b47c22 100644 --- a/frontend/src/mocks/handlers/articles.ts +++ b/frontend/src/mocks/handlers/articles.ts @@ -10,10 +10,6 @@ import { ArticleType } from '../../models/Article'; const articleUrl = 'https://think0wise.tistory.com/107'; export const articlesHandler = [ - // rest.get(`${BASE_URL}/articles`, (req, res, ctx) => { - // return res(ctx.status(200), ctx.json(articles)); - // }), - rest.get(`${BASE_URL}/meta-og?url=${articleUrl}`, async (req, res, ctx) => { const data = metaOg; @@ -28,6 +24,7 @@ export const articlesHandler = [ url: 'https://think0wise.tistory.com/107', createdAt: '2023-07-08 16:48', isBookMarked: false, + isLiked: false, imageUrl: 'https://plus.unsplash.com/premium_photo-1682088845396-1b310a002302?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8MXx8JUVDJTk1JTg0JUVEJThCJUIwJUVEJTgxJUI0fGVufDB8fDB8fHww&auto=format&fit=crop&w=500&q=60', }; @@ -40,6 +37,10 @@ export const articlesHandler = [ return res(ctx.status(200)); }), + rest.put(`${BASE_URL}/articles/:articleId/like`, (req, res, ctx) => { + return res(ctx.status(200)); + }), + rest.get(`${BASE_URL}/articles`, (req, res, ctx) => { const course = req.url.searchParams.get('course') ?? 'all'; const onlyBookmarked = req.url.searchParams.get('onlyBookmarked') as string; diff --git a/frontend/src/models/Article.ts b/frontend/src/models/Article.ts index dc7540160..67b99fba9 100644 --- a/frontend/src/models/Article.ts +++ b/frontend/src/models/Article.ts @@ -9,6 +9,7 @@ export interface ArticleType { userName: string; title: string; isBookMarked: boolean; + isLiked: boolean; url: string; createdAt: string; imageUrl: string; @@ -28,6 +29,11 @@ export interface ArticleBookmarkPutRequest { bookmark: boolean; } +export interface ArticleLikePutRequest { + articleId: number; + like: boolean; +} + export type Course = '프론트엔드' | '백엔드' | '안드로이드'; export type CourseFilter = Course | '전체보기'; From 496eeec28ee9b92f9ed4aa9d262b4edfc5e30e8e Mon Sep 17 00:00:00 2001 From: hong-sile Date: Wed, 27 Sep 2023 11:00:47 +0900 Subject: [PATCH 46/71] =?UTF-8?q?test:=20=EB=B3=80=EA=B2=BD=EB=90=9C=20api?= =?UTF-8?q?=20=EB=AA=85=EC=84=B8=EC=97=90=20=EB=A7=9E=EA=B2=8C=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20=EC=BD=94=EB=93=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/wooteco/prolog/docu/ArticleDocumentation.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/documentation/java/wooteco/prolog/docu/ArticleDocumentation.java b/backend/src/documentation/java/wooteco/prolog/docu/ArticleDocumentation.java index 2bab040a4..6aaac171b 100644 --- a/backend/src/documentation/java/wooteco/prolog/docu/ArticleDocumentation.java +++ b/backend/src/documentation/java/wooteco/prolog/docu/ArticleDocumentation.java @@ -48,7 +48,7 @@ public class ArticleDocumentation extends NewDocumentation { .header("Authorization", "Bearer " + accessToken) .contentType(APPLICATION_JSON) .body(articleLikesRequest) - .when().put("/articles/{article-id}/likes", 1L) + .when().put("/articles/{article-id}/like", 1L) .then().log().all(); //then From c024a7fc52f2af30345f824627cbc386dbff5ed3 Mon Sep 17 00:00:00 2001 From: hong-sile Date: Wed, 27 Sep 2023 16:11:06 +0900 Subject: [PATCH 47/71] =?UTF-8?q?fix:=20=EC=9D=B8=EC=88=98=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20=EC=98=A4=EB=A5=98=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/wooteco/prolog/steps/ArticleStepDefinitions.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/acceptanceTest/java/wooteco/prolog/steps/ArticleStepDefinitions.java b/backend/src/acceptanceTest/java/wooteco/prolog/steps/ArticleStepDefinitions.java index 4b53ca4c3..2691c3d26 100644 --- a/backend/src/acceptanceTest/java/wooteco/prolog/steps/ArticleStepDefinitions.java +++ b/backend/src/acceptanceTest/java/wooteco/prolog/steps/ArticleStepDefinitions.java @@ -123,7 +123,7 @@ public class ArticleStepDefinitions extends AcceptanceSteps { //final String articleUrl = context.response.header("Location"); final ArticleBookmarkRequest request = new ArticleBookmarkRequest(true); context.invokeHttpPutWithToken( - String.format("/articles/%d/likes", articleId), + String.format("/articles/%d/like", articleId), request ); } From 2811fc42ef8436f74ad6405b54160fd7ad587fc2 Mon Sep 17 00:00:00 2001 From: hae-on Date: Wed, 27 Sep 2023 16:28:15 +0900 Subject: [PATCH 48/71] =?UTF-8?q?refactor:=20authorization=20null=20?= =?UTF-8?q?=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/apis/index.ts | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/frontend/src/apis/index.ts b/frontend/src/apis/index.ts index b02cb209f..d7908c2b8 100644 --- a/frontend/src/apis/index.ts +++ b/frontend/src/apis/index.ts @@ -2,9 +2,13 @@ import axios from 'axios'; import { BASE_URL } from '../configs/environment'; import LOCAL_STORAGE_KEY from '../constants/localStorage'; +const accessToken = localStorage.getItem(LOCAL_STORAGE_KEY.ACCESS_TOKEN); + +const headers = { + Authorization: `Bearer ${accessToken}`, +}; + export const client = axios.create({ baseURL: BASE_URL, - headers: { - Authorization: `Bearer ${localStorage.getItem(LOCAL_STORAGE_KEY.ACCESS_TOKEN)}`, - }, + headers: accessToken ? headers : {}, }); From 123c9bfde773dad1f68974103309d5983a284424 Mon Sep 17 00:00:00 2001 From: donghae-kim Date: Wed, 4 Oct 2023 17:32:18 +0900 Subject: [PATCH 49/71] =?UTF-8?q?feat:=20=EC=95=84=ED=8B=B0=ED=81=B4=20?= =?UTF-8?q?=EC=A1=B0=ED=9A=8C=EC=88=98=20=EA=B8=B0=EB=8A=A5=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../article/application/ArticleService.java | 7 +++++ .../prolog/article/domain/Article.java | 29 ++++++++++++------- .../prolog/article/domain/ViewCount.java | 23 +++++++++++++++ .../prolog/article/ui/ArticleController.java | 6 ++++ .../prod/V8__alter_table_article.sql | 2 ++ 5 files changed, 57 insertions(+), 10 deletions(-) create mode 100644 backend/src/main/java/wooteco/prolog/article/domain/ViewCount.java create mode 100644 backend/src/main/resources/db/migration/prod/V8__alter_table_article.sql diff --git a/backend/src/main/java/wooteco/prolog/article/application/ArticleService.java b/backend/src/main/java/wooteco/prolog/article/application/ArticleService.java index abefe800c..afcc1aaca 100644 --- a/backend/src/main/java/wooteco/prolog/article/application/ArticleService.java +++ b/backend/src/main/java/wooteco/prolog/article/application/ArticleService.java @@ -90,4 +90,11 @@ public List getFilteredArticles(final LoginMember member, .map(article -> ArticleResponse.of(article, member.getId())) .collect(toList()); } + + @Transactional + public void updateViewCount(final Long id) { + final Article article = articleRepository.findById(id) + .orElseThrow(() -> new BadRequestException(ARTICLE_NOT_FOUND_EXCEPTION)); + article.updateViewCount(); + } } diff --git a/backend/src/main/java/wooteco/prolog/article/domain/Article.java b/backend/src/main/java/wooteco/prolog/article/domain/Article.java index be1c9ac0d..36cca7134 100644 --- a/backend/src/main/java/wooteco/prolog/article/domain/Article.java +++ b/backend/src/main/java/wooteco/prolog/article/domain/Article.java @@ -1,8 +1,14 @@ package wooteco.prolog.article.domain; -import static java.lang.Boolean.TRUE; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.jpa.domain.support.AuditingEntityListener; +import wooteco.prolog.common.exception.BadRequestCode; +import wooteco.prolog.common.exception.BadRequestException; +import wooteco.prolog.member.domain.Member; -import java.time.LocalDateTime; import javax.persistence.Embedded; import javax.persistence.Entity; import javax.persistence.EntityListeners; @@ -12,14 +18,9 @@ import javax.persistence.Id; import javax.persistence.JoinColumn; import javax.persistence.ManyToOne; -import lombok.AccessLevel; -import lombok.Getter; -import lombok.NoArgsConstructor; -import org.springframework.data.annotation.CreatedDate; -import org.springframework.data.jpa.domain.support.AuditingEntityListener; -import wooteco.prolog.common.exception.BadRequestCode; -import wooteco.prolog.common.exception.BadRequestException; -import wooteco.prolog.member.domain.Member; +import java.time.LocalDateTime; + +import static java.lang.Boolean.TRUE; @Entity @Getter @@ -53,6 +54,9 @@ public class Article { @CreatedDate private LocalDateTime createdAt; + @Embedded + private ViewCount views; + public Article(final Member member, final Title title, final Url url, final ImageUrl imageUrl) { this.member = member; this.title = title; @@ -60,6 +64,7 @@ public Article(final Member member, final Title title, final Url url, final Imag this.imageUrl = imageUrl; this.articleBookmarks = new ArticleBookmarks(); this.articleLikes = new ArticleLikes(); + this.views = new ViewCount(); } public void validateOwner(final Member member) { @@ -106,4 +111,8 @@ private void addLike(final Member member) { private void removeLike(final Member member) { articleLikes.removeLike(member.getId()); } + + public void updateViewCount() { + this.views.increase(); + } } diff --git a/backend/src/main/java/wooteco/prolog/article/domain/ViewCount.java b/backend/src/main/java/wooteco/prolog/article/domain/ViewCount.java new file mode 100644 index 000000000..9a815e2bf --- /dev/null +++ b/backend/src/main/java/wooteco/prolog/article/domain/ViewCount.java @@ -0,0 +1,23 @@ +package wooteco.prolog.article.domain; + +import lombok.AccessLevel; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.ToString; + +import javax.persistence.Embeddable; + +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@EqualsAndHashCode +@ToString +@Embeddable +public class ViewCount { + + private int views; + + public void increase() { + this.views++; + } +} diff --git a/backend/src/main/java/wooteco/prolog/article/ui/ArticleController.java b/backend/src/main/java/wooteco/prolog/article/ui/ArticleController.java index 91ade747e..0de2d2592 100644 --- a/backend/src/main/java/wooteco/prolog/article/ui/ArticleController.java +++ b/backend/src/main/java/wooteco/prolog/article/ui/ArticleController.java @@ -72,4 +72,10 @@ public ResponseEntity> getFilteredArticles(@AuthMemberPrin return ResponseEntity.ok(articleResponses); } + + @PostMapping("/{id}/views") + public ResponseEntity updateViewCount(@PathVariable final Long id) { + articleService.updateViewCount(id); + return ResponseEntity.ok().build(); + } } diff --git a/backend/src/main/resources/db/migration/prod/V8__alter_table_article.sql b/backend/src/main/resources/db/migration/prod/V8__alter_table_article.sql new file mode 100644 index 000000000..286616e9f --- /dev/null +++ b/backend/src/main/resources/db/migration/prod/V8__alter_table_article.sql @@ -0,0 +1,2 @@ +ALTER TABLE prolog.article + ADD COLUMN views int default 0; From 888ba0e0e03b70c94dd8f73808f109211767110d Mon Sep 17 00:00:00 2001 From: donghae-kim Date: Wed, 4 Oct 2023 17:32:40 +0900 Subject: [PATCH 50/71] =?UTF-8?q?test:=20=EC=95=84=ED=8B=B0=ED=81=B4=20?= =?UTF-8?q?=EC=A1=B0=ED=9A=8C=EC=88=98=20=EB=8B=A8=EC=9C=84=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../article/application/ArticleServiceTest.java | 16 ++++++++++++++++ .../prolog/article/domain/ArticleTest.java | 13 +++++++++++++ 2 files changed, 29 insertions(+) diff --git a/backend/src/test/java/wooteco/prolog/article/application/ArticleServiceTest.java b/backend/src/test/java/wooteco/prolog/article/application/ArticleServiceTest.java index 0512583d5..bc867ea1d 100644 --- a/backend/src/test/java/wooteco/prolog/article/application/ArticleServiceTest.java +++ b/backend/src/test/java/wooteco/prolog/article/application/ArticleServiceTest.java @@ -329,4 +329,20 @@ void filter_isBookmarked() { verify(articleRepository).findArticlesByCourseAndMember(any(), any()); Assertions.assertThat(articleResponses.get(0).getTitle()).isEqualTo(article.getTitle().getTitle()); } + + @DisplayName("아티클 조회시 조회수가 증가한다.") + @Test + void updateViews() { + //given + final Member member = new Member(1L, "username", "nickname", CREW, 1L, "url"); + final Article article = new Article(member, new Title("title"), new Url("url"), new ImageUrl("imageUrl")); + final Long articleId = 1L; + when(articleRepository.findById(articleId)).thenReturn(Optional.of(article)); + + //when + articleService.updateViewCount(articleId); + + //then + Assertions.assertThat(article.getViews().getViews()).isEqualTo(1); + } } diff --git a/backend/src/test/java/wooteco/prolog/article/domain/ArticleTest.java b/backend/src/test/java/wooteco/prolog/article/domain/ArticleTest.java index 5f6bbedca..8c180e95b 100644 --- a/backend/src/test/java/wooteco/prolog/article/domain/ArticleTest.java +++ b/backend/src/test/java/wooteco/prolog/article/domain/ArticleTest.java @@ -178,4 +178,17 @@ void removeLike() { .isFalse(); } } + + @DisplayName("아티클 조회시 조회수가 증가한다.") + @Test + void updateViewCount() { + //given + final Article article = new Article(member, title, url, imageUrl); + + //when + article.updateViewCount(); + + //then + assertThat(article.getViews().getViews()).isEqualTo(1); + } } From a84319631beff29b9cc0befd651e81345c633870 Mon Sep 17 00:00:00 2001 From: donghae-kim Date: Wed, 4 Oct 2023 17:32:58 +0900 Subject: [PATCH 51/71] =?UTF-8?q?test:=20=EC=95=84=ED=8B=B0=ED=81=B4=20?= =?UTF-8?q?=EC=A1=B0=ED=9A=8C=EC=88=98=20=EC=9D=B8=EC=88=98=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../prolog/steps/ArticleStepDefinitions.java | 15 +++++++++++++++ .../resources/wooteco/prolog/article.feature | 5 +++++ 2 files changed, 20 insertions(+) diff --git a/backend/src/acceptanceTest/java/wooteco/prolog/steps/ArticleStepDefinitions.java b/backend/src/acceptanceTest/java/wooteco/prolog/steps/ArticleStepDefinitions.java index 2691c3d26..6f4ca13b2 100644 --- a/backend/src/acceptanceTest/java/wooteco/prolog/steps/ArticleStepDefinitions.java +++ b/backend/src/acceptanceTest/java/wooteco/prolog/steps/ArticleStepDefinitions.java @@ -136,4 +136,19 @@ public class ArticleStepDefinitions extends AcceptanceSteps { ); //아티클 단건 조회에 좋아요 여부가 추가되면 단건 조회로 likes 여부까지 검증해보기 } + + @When("{long}번 아티클에 조회수 추가요청을 보내면") + public void 아티클에_조회수_추가요청을_보내면(final Long articleId) { + context.invokeHttpPost( + String.format("/articles/%d/views", articleId),"" + ); + } + + @Then("아티클의 조회수가 증가한다") + public void 아티클의_조회수가_증가한다() { + final int statusCode = context.response.statusCode(); + assertAll( + () -> assertThat(statusCode).isEqualTo(HttpStatus.OK.value()) + ); + } } diff --git a/backend/src/acceptanceTest/resources/wooteco/prolog/article.feature b/backend/src/acceptanceTest/resources/wooteco/prolog/article.feature index 0c21cddc5..74cc26e49 100644 --- a/backend/src/acceptanceTest/resources/wooteco/prolog/article.feature +++ b/backend/src/acceptanceTest/resources/wooteco/prolog/article.feature @@ -36,3 +36,8 @@ Feature: 아티클 관련 기능 Given 아티클이 작성되어 있고 When 1번 아티클에 좋아요 요청을 보내면 Then 아티클에 좋아요가 등록된다 + + Scenario: 아티클 조회수 추가하기 + Given 아티클이 작성되어 있고 + When 1번 아티클에 조회수 추가요청을 보내면 + Then 아티클의 조회수가 증가한다 From 8b5029c042d6573fda7cc1778fe896582f0ee2e8 Mon Sep 17 00:00:00 2001 From: donghae-kim Date: Wed, 4 Oct 2023 17:33:03 +0900 Subject: [PATCH 52/71] =?UTF-8?q?test:=20=EC=95=84=ED=8B=B0=ED=81=B4=20?= =?UTF-8?q?=EC=A1=B0=ED=9A=8C=EC=88=98=20=EB=AC=B8=EC=84=9C=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/src/documentation/adoc/article.adoc | 10 ++++++++++ .../wooteco/prolog/docu/ArticleDocumentation.java | 14 ++++++++++++++ 2 files changed, 24 insertions(+) diff --git a/backend/src/documentation/adoc/article.adoc b/backend/src/documentation/adoc/article.adoc index 052ed0ea2..0aabfe86b 100644 --- a/backend/src/documentation/adoc/article.adoc +++ b/backend/src/documentation/adoc/article.adoc @@ -20,3 +20,13 @@ include::{snippets}/article/likes/http-request.adoc[] ==== Response include::{snippets}/article/likes/http-response.adoc[] + +=== 아티클 조회수 추가 + +==== Request + +include::{snippets}/article/views/http-request.adoc[] + +==== Response + +include::{snippets}/article/views/http-response.adoc[] diff --git a/backend/src/documentation/java/wooteco/prolog/docu/ArticleDocumentation.java b/backend/src/documentation/java/wooteco/prolog/docu/ArticleDocumentation.java index 6aaac171b..1237ef2ba 100644 --- a/backend/src/documentation/java/wooteco/prolog/docu/ArticleDocumentation.java +++ b/backend/src/documentation/java/wooteco/prolog/docu/ArticleDocumentation.java @@ -57,4 +57,18 @@ public class ArticleDocumentation extends NewDocumentation { //docs response.apply(document("article/like")); } + + @Test + void 아티클의_조회수를_증가시킨다() { + //given, when + final ValidatableMockMvcResponse response = given + .when().post("/articles/{article-id}/views", 1L) + .then().log().all(); + + //then + response.expect(status().isOk()); + + //docs + response.apply(document("article/views")); + } } From 426afe0b8a21fbfa1fa89d5ea7d5cbef3e53313f Mon Sep 17 00:00:00 2001 From: hae-on Date: Wed, 4 Oct 2023 18:07:08 +0900 Subject: [PATCH 53/71] =?UTF-8?q?feat:=20=EB=B9=84=ED=81=AC=EB=A3=A8=20?= =?UTF-8?q?=ED=9A=8C=EC=9B=90=20=EC=95=84=ED=8B=B0=ED=81=B4=20=EC=9E=91?= =?UTF-8?q?=EC=84=B1=20=EB=AA=BB=ED=95=98=EB=8F=84=EB=A1=9D=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: 이도현 --- frontend/src/contexts/UserProvider.js | 2 +- frontend/src/pages/ArticleListPage/index.tsx | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/frontend/src/contexts/UserProvider.js b/frontend/src/contexts/UserProvider.js index edf55604c..84ad18af8 100644 --- a/frontend/src/contexts/UserProvider.js +++ b/frontend/src/contexts/UserProvider.js @@ -10,7 +10,7 @@ const DEFAULT_USER = { userId: null, username: null, nickname: null, - role: null, + role: '', imageUrl: null, accessToken: null, isLoggedIn: false, diff --git a/frontend/src/pages/ArticleListPage/index.tsx b/frontend/src/pages/ArticleListPage/index.tsx index 9f6882c0e..7efff3ba7 100644 --- a/frontend/src/pages/ArticleListPage/index.tsx +++ b/frontend/src/pages/ArticleListPage/index.tsx @@ -30,7 +30,9 @@ const ArticleListPage = () => { const [checked, setChecked] = useState(false); const { user } = useContext(UserContext); - const { isLoggedIn } = user; + const { isLoggedIn, role } = user; + + const authorized = role === 'CREW' && isLoggedIn; const { data: filteredArticles = [], refetch: getFilteredArticles } = useGetFilteredArticleQuery( selectedCourse.value, @@ -66,7 +68,7 @@ const ArticleListPage = () => { )} - {isLoggedIn && ( + {authorized && ( )} @@ -102,6 +103,10 @@ const FilteringWrapper = styled.div` const SelectBoxWrapper = styled.div` width: 150px; + + ${MEDIA_QUERY.sm} { + width: 130px; + } `; export const WriteButtonStyle = css` @@ -117,4 +122,18 @@ export const WriteButtonStyle = css` :hover { background-color: ${COLOR.DARK_BLUE_600}; } + + ${MEDIA_QUERY.sm} { + width: 42px; + + & > img { + margin: 0; + } + } +`; + +const ButtonText = styled.span` + ${MEDIA_QUERY.sm} { + display: none; + } `;