From ff26beea608ce1e01c8ec405ca279b8ca97965f2 Mon Sep 17 00:00:00 2001 From: hae-on <solbi2004@naver.com> Date: Wed, 13 Sep 2023 18:04:30 +0900 Subject: [PATCH 01/16] =?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: 이도현 <Creative-Lee@users.noreply.github.com> --- .../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<SelectBoxProps> = ({ + isClearable = true, isMulti = false, options, placeholder, @@ -75,7 +76,7 @@ const SelectBox: React.VFC<SelectBoxProps> = ({ `} > <Select - isClearable={true} + isClearable={isClearable} isMulti={isMulti} options={options} placeholder={placeholder} @@ -83,7 +84,6 @@ const SelectBox: React.VFC<SelectBoxProps> = ({ styles={selectStyles} defaultValue={defaultOption} value={value} - // theme={(theme) => ({ ...theme, colors: { ...theme.colors, primary: 'transparent' } })} /> </div> ); diff --git a/frontend/src/pages/ArticleListPage/index.tsx b/frontend/src/pages/ArticleListPage/index.tsx index 4c4f8cc2c..d1db4802b 100644 --- a/frontend/src/pages/ArticleListPage/index.tsx +++ b/frontend/src/pages/ArticleListPage/index.tsx @@ -4,23 +4,50 @@ import ArticleList from '../../components/Article/ArticleList'; import { Button } from '../../components'; import PencilIcon from '../../assets/images/pencil_icon.svg'; import { useHistory } from 'react-router-dom'; -import { PATH } from '../../constants'; +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 { css } from '@emotion/react'; + +const CATEGORY_OPTIONS = [ + { value: '전체보기', label: '전체보기' }, + { value: '프론트엔드', label: '프론트엔드' }, + { value: '백엔드', label: '백엔드' }, + { value: '안드로이드', label: '안드로이드' }, +]; + +type CategoryOptions = typeof CATEGORY_OPTIONS[number]; const ArticleListPage = () => { const history = useHistory(); const goNewArticlePage = () => history.push(PATH.NEW_ARTICLE); + const [selectedOption, setSelectedOption] = useState<CategoryOptions>(CATEGORY_OPTIONS[0]); + + const changeFilterOption: (option: { value: string; label: string }) => void = (option) => { + setSelectedOption(option); + }; return ( <div css={[MainContentStyle]}> <Container> + <SelectBoxWrapper> + <SelectBox + isClearable={false} + value={selectedOption} + defaultOption={selectedOption} + options={CATEGORY_OPTIONS} + onChange={changeFilterOption} + /> + </SelectBoxWrapper> <Button type="button" - size="SMALL" + size="X_SMALL" icon={PencilIcon} alt="새 아티클 쓰기 아이콘" onClick={goNewArticlePage} + cssProps={WriteButtonStyle} > 글쓰기 </Button> @@ -34,7 +61,27 @@ export default ArticleListPage; export const Container = styled.div` display: flex; - flex-direction: column; - align-items: flex-end; + flex-direction: row; + justify-content: space-between; margin-bottom: 20px; + gap: 15px; +`; + +const SelectBoxWrapper = styled.div` + width: 200px; +`; + +export const WriteButtonStyle = css` + width: 100px; + height: 42px; + padding: 0.2rem 0.8rem; + + border-radius: 0.6rem; + font-size: 1.6rem; + + color: ${COLOR.WHITE}; + + :hover { + background-color: ${COLOR.DARK_BLUE_600}; + } `; From c4e1f9885f9516608f63e959be55e0a1df383648 Mon Sep 17 00:00:00 2001 From: hae-on <solbi2004@naver.com> Date: Fri, 15 Sep 2023 16:50:31 +0900 Subject: [PATCH 02/16] =?UTF-8?q?feat:=20=EC=8A=A4=ED=81=AC=EB=9E=A9=20?= =?UTF-8?q?=EB=B2=84=ED=8A=BC=20=EC=B6=94=EA=B0=80=EC=99=80=20=EB=AF=B8?= =?UTF-8?q?=EC=84=B8=20=EC=8A=A4=ED=83=80=EC=9D=BC=20=EC=A1=B0=EC=A0=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: 이도현 <Creative-Lee@users.noreply.github.com> --- .../src/components/Article/Article.style.tsx | 61 +++++++++++++------ frontend/src/components/Article/Article.tsx | 20 +++++- .../components/Article/ArticleList.style.tsx | 12 ++-- .../src/components/Reaction/Scrap.styles.ts | 2 +- frontend/src/components/Reaction/Scrap.tsx | 8 ++- frontend/src/hooks/queries/article.ts | 1 - frontend/src/pages/ArticleListPage/index.tsx | 2 +- 7 files changed, 74 insertions(+), 32 deletions(-) diff --git a/frontend/src/components/Article/Article.style.tsx b/frontend/src/components/Article/Article.style.tsx index 049d4cba4..a7ffa518b 100644 --- a/frontend/src/components/Article/Article.style.tsx +++ b/frontend/src/components/Article/Article.style.tsx @@ -1,11 +1,12 @@ import styled from '@emotion/styled'; +import { css } from '@emotion/react'; import { COLOR } from '../../constants'; export const Container = styled.li` width: 100%; - height: 340px; - padding: 20px; - border-radius: 15px; + height: 100%; + padding: 10px; + border-radius: 8px; background-color: #ffffff; list-style: none; @@ -20,16 +21,16 @@ export const ThumbnailWrapper = styled.div` display: flex; justify-content: center; align-items: center; + aspect-ratio: 16/9; width: 100%; - height: 154px; border-radius: 15px; - margin-bottom: 20px; + margin-bottom: 10px; `; export const Thumbnail = styled.img` width: 100%; - height: 154px; - border-radius: 15px; + height: 100%; + border-radius: 8px; object-fit: cover; `; @@ -41,34 +42,58 @@ export const ArticleInfoContainer = styled.div` padding: 10px; `; -export const UserName = styled.p` - width: 250px; - margin: 0; - color: ${COLOR.DARK_GRAY_400}; - font-size: 14px; +export const ArticleInfoWrapper = styled.div` + display: flex; + width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; word-break: break-all; + font-size: 14px; +`; + +export const UserName = styled.p` + margin: 0; + color: ${COLOR.DARK_GRAY_400}; `; export const Title = styled.p` - width: 250px; + width: 100%; + height: 50px; margin: 0; color: ${COLOR.BLACK_900}; font-size: 16px; font-weight: 700; - overflow: hidden; text-overflow: ellipsis; - white-space: nowrap; - word-break: break-all; + overflow: hidden; + word-break: break-word; + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; +`; + +export const ScrapButtonWrapper = styled.div``; + +export const ArticleScrapButtonStyle = css` + flex-direction: column; + padding: 0; + width: fit-content; + font-size: 1.4rem; + + background-color: transparent; + color: ${COLOR.BLACK_800}; + + & > img { + margin-right: 0; + width: 1.8rem; + height: 1.8rem; + } `; export const CreatedAt = styled.span` width: 100%; - margin-top: 16px; color: ${COLOR.DARK_GRAY_400}; text-align: right; - font-size: 16px; + font-size: 12px; font-weight: 700; `; diff --git a/frontend/src/components/Article/Article.tsx b/frontend/src/components/Article/Article.tsx index c610c38af..eccc0722d 100644 --- a/frontend/src/components/Article/Article.tsx +++ b/frontend/src/components/Article/Article.tsx @@ -1,7 +1,18 @@ import * as Styled from './Article.style'; import type { ArticleType } from '../../models/Article'; +import Scrap from '../Reaction/Scrap'; +import { useState } from 'react'; const Article = ({ title, userName, url, createdAt, imageUrl }: ArticleType) => { + const [scrap, setScrap] = useState(false); + + const toggleScrap: React.MouseEventHandler<HTMLButtonElement> = (e) => { + e.preventDefault(); + setScrap((prev) => !prev); + + // api + }; + return ( <Styled.Container> <Styled.Anchor href={url} target="_blank" rel="noopener noreferrer"> @@ -9,9 +20,14 @@ const Article = ({ title, userName, url, createdAt, imageUrl }: ArticleType) => <Styled.Thumbnail src={imageUrl} /> </Styled.ThumbnailWrapper> <Styled.ArticleInfoContainer> - <Styled.UserName>{userName}</Styled.UserName> + <Styled.ArticleInfoWrapper> + <Styled.UserName>{userName}</Styled.UserName> + <Styled.CreatedAt>{createdAt.split(' ')[0]}</Styled.CreatedAt> + </Styled.ArticleInfoWrapper> <Styled.Title>{title}</Styled.Title> - <Styled.CreatedAt>{createdAt}</Styled.CreatedAt> + <Styled.ScrapButtonWrapper> + <Scrap scrap={scrap} onClick={toggleScrap} cssProps={Styled.ArticleScrapButtonStyle} /> + </Styled.ScrapButtonWrapper> </Styled.ArticleInfoContainer> </Styled.Anchor> </Styled.Container> diff --git a/frontend/src/components/Article/ArticleList.style.tsx b/frontend/src/components/Article/ArticleList.style.tsx index b7d21cadd..e941cfc56 100644 --- a/frontend/src/components/Article/ArticleList.style.tsx +++ b/frontend/src/components/Article/ArticleList.style.tsx @@ -3,19 +3,19 @@ import MEDIA_QUERY from '../../constants/mediaQuery'; export const Container = styled.ul` display: grid; - grid-template-columns: repeat(3, 1fr); + grid-template-columns: repeat(4, 1fr); grid-auto-rows: 1fr; - gap: 30px 64px; + gap: 10px; - ${MEDIA_QUERY.xl} { - gap: 30px 40px; + ${MEDIA_QUERY.lg} { + grid-template-columns: repeat(3, 1fr); } - ${MEDIA_QUERY.lg} { + ${MEDIA_QUERY.md} { grid-template-columns: repeat(2, 1fr); } - ${MEDIA_QUERY.md} { + ${MEDIA_QUERY.sm} { grid-template-columns: repeat(1, 1fr); } `; diff --git a/frontend/src/components/Reaction/Scrap.styles.ts b/frontend/src/components/Reaction/Scrap.styles.ts index 2335d252a..976533905 100644 --- a/frontend/src/components/Reaction/Scrap.styles.ts +++ b/frontend/src/components/Reaction/Scrap.styles.ts @@ -1,7 +1,7 @@ import { css } from '@emotion/react'; import { COLOR } from '../../enumerations/color'; -export const ScrapButtonStyle = css` +export const DefaultScrapButtonStyle = css` flex-direction: column; padding: 0; width: fit-content; diff --git a/frontend/src/components/Reaction/Scrap.tsx b/frontend/src/components/Reaction/Scrap.tsx index 08bfad860..9a30aa9ec 100644 --- a/frontend/src/components/Reaction/Scrap.tsx +++ b/frontend/src/components/Reaction/Scrap.tsx @@ -3,14 +3,16 @@ import { Button, BUTTON_SIZE } from '..'; import scrappedIcon from '../../assets/images/scrap_filled.svg'; import unScrapIcon from '../../assets/images/scrap.svg'; -import { ScrapButtonStyle } from './Scrap.styles'; +import { DefaultScrapButtonStyle } from './Scrap.styles'; +import { SerializedStyles } from '@emotion/react'; interface Props { scrap: boolean; onClick: MouseEventHandler<HTMLButtonElement>; + cssProps?: SerializedStyles; } -const Scrap = ({ scrap, onClick }: Props) => { +const Scrap = ({ scrap, onClick, cssProps }: Props) => { const scrapIcon = scrap ? scrappedIcon : unScrapIcon; const scrapIconAlt = scrap ? '스크랩 취소' : '스크랩'; @@ -20,7 +22,7 @@ const Scrap = ({ scrap, onClick }: Props) => { size={BUTTON_SIZE.X_SMALL} icon={scrapIcon} alt={scrapIconAlt} - cssProps={ScrapButtonStyle} + cssProps={cssProps ?? DefaultScrapButtonStyle} onClick={onClick} /> ); diff --git a/frontend/src/hooks/queries/article.ts b/frontend/src/hooks/queries/article.ts index 4e6780cbf..54fd5f804 100644 --- a/frontend/src/hooks/queries/article.ts +++ b/frontend/src/hooks/queries/article.ts @@ -1,5 +1,4 @@ import { useMutation, useQuery, useQueryClient } from 'react-query'; -import { UserContext } from '../../contexts/UserProvider'; import { requestGetArticles, requestPostArticles } from '../../apis/articles'; import { ArticleType } from '../../models/Article'; import { ERROR_MESSAGE } from '../../constants'; diff --git a/frontend/src/pages/ArticleListPage/index.tsx b/frontend/src/pages/ArticleListPage/index.tsx index d1db4802b..e4f55745c 100644 --- a/frontend/src/pages/ArticleListPage/index.tsx +++ b/frontend/src/pages/ArticleListPage/index.tsx @@ -68,7 +68,7 @@ export const Container = styled.div` `; const SelectBoxWrapper = styled.div` - width: 200px; + width: 150px; `; export const WriteButtonStyle = css` From af831690cf2edb352300a0c9e2d7c7e21fda6c4f Mon Sep 17 00:00:00 2001 From: hae-on <solbi2004@naver.com> Date: Fri, 15 Sep 2023 17:22:51 +0900 Subject: [PATCH 03/16] =?UTF-8?q?feat:=20=EC=95=84=ED=8B=B0=ED=81=B4=20?= =?UTF-8?q?=EB=B6=81=EB=A7=88=ED=81=AC=20=EA=B8=B0=EB=8A=A5=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 --- frontend/src/apis/articles.ts | 11 +++++++- .../src/components/Article/Article.style.tsx | 4 +-- frontend/src/components/Article/Article.tsx | 28 +++++++++++++------ frontend/src/hooks/queries/article.ts | 13 ++++++++- frontend/src/mocks/db/articles.json | 16 +++++++++++ frontend/src/mocks/handlers/articles.ts | 4 +++ frontend/src/models/Article.ts | 5 ++++ 7 files changed, 67 insertions(+), 14 deletions(-) diff --git a/frontend/src/apis/articles.ts b/frontend/src/apis/articles.ts index f45c3bd81..6982ceddb 100644 --- a/frontend/src/apis/articles.ts +++ b/frontend/src/apis/articles.ts @@ -1,5 +1,10 @@ import { client } from '.'; -import { ArticleRequest, MetaOgRequest, MetaOgResponse } from '../models/Article'; +import { + ArticleBookmarkPutRequest, + ArticleRequest, + MetaOgRequest, + MetaOgResponse, +} from '../models/Article'; export const requestGetArticles = () => client.get(`/articles`); @@ -8,3 +13,7 @@ export const requestPostArticles = (body: ArticleRequest) => client.post('/artic export const requestGetMetaOg = ({ url }: MetaOgRequest) => { return client.get<MetaOgResponse>(`/meta-og?url=${url}`); }; + +export const requestPutArticleBookmark = ({ articleId, bookmark }: ArticleBookmarkPutRequest) => { + return client.put(`/articles/${articleId}/bookmark`, { checked: bookmark }); +}; diff --git a/frontend/src/components/Article/Article.style.tsx b/frontend/src/components/Article/Article.style.tsx index a7ffa518b..14e03d311 100644 --- a/frontend/src/components/Article/Article.style.tsx +++ b/frontend/src/components/Article/Article.style.tsx @@ -72,9 +72,7 @@ export const Title = styled.p` -webkit-box-orient: vertical; `; -export const ScrapButtonWrapper = styled.div``; - -export const ArticleScrapButtonStyle = css` +export const ArticleBookmarkButtonStyle = css` flex-direction: column; padding: 0; width: fit-content; diff --git a/frontend/src/components/Article/Article.tsx b/frontend/src/components/Article/Article.tsx index eccc0722d..4211117b4 100644 --- a/frontend/src/components/Article/Article.tsx +++ b/frontend/src/components/Article/Article.tsx @@ -1,16 +1,24 @@ import * as Styled from './Article.style'; import type { ArticleType } from '../../models/Article'; import Scrap from '../Reaction/Scrap'; -import { useState } from 'react'; +import { useRef, useState } from 'react'; +import { usePutArticleBookmarkMutation } from '../../hooks/queries/article'; +import debounce from '../../utils/debounce'; -const Article = ({ title, userName, url, createdAt, imageUrl }: ArticleType) => { - const [scrap, setScrap] = useState(false); +const Article = ({ id, title, userName, url, createdAt, imageUrl }: ArticleType) => { + const bookmarkRef = useRef(false); + const [bookmark, setBookmark] = useState(false); + const { mutate: putBookmark } = usePutArticleBookmarkMutation(); - const toggleScrap: React.MouseEventHandler<HTMLButtonElement> = (e) => { + const toggleBookmark: React.MouseEventHandler<HTMLButtonElement> = (e) => { e.preventDefault(); - setScrap((prev) => !prev); - // api + bookmarkRef.current = !bookmarkRef.current; + setBookmark((prev) => !prev); + + debounce(() => { + putBookmark({ articleId: id, bookmark: bookmarkRef.current }); + }, 300); }; return ( @@ -25,9 +33,11 @@ const Article = ({ title, userName, url, createdAt, imageUrl }: ArticleType) => <Styled.CreatedAt>{createdAt.split(' ')[0]}</Styled.CreatedAt> </Styled.ArticleInfoWrapper> <Styled.Title>{title}</Styled.Title> - <Styled.ScrapButtonWrapper> - <Scrap scrap={scrap} onClick={toggleScrap} cssProps={Styled.ArticleScrapButtonStyle} /> - </Styled.ScrapButtonWrapper> + <Scrap + scrap={bookmark} + onClick={toggleBookmark} + cssProps={Styled.ArticleBookmarkButtonStyle} + /> </Styled.ArticleInfoContainer> </Styled.Anchor> </Styled.Container> diff --git a/frontend/src/hooks/queries/article.ts b/frontend/src/hooks/queries/article.ts index 54fd5f804..cc4eded62 100644 --- a/frontend/src/hooks/queries/article.ts +++ b/frontend/src/hooks/queries/article.ts @@ -1,5 +1,9 @@ import { useMutation, useQuery, useQueryClient } from 'react-query'; -import { requestGetArticles, requestPostArticles } from '../../apis/articles'; +import { + requestGetArticles, + requestPostArticles, + requestPutArticleBookmark, +} from '../../apis/articles'; import { ArticleType } from '../../models/Article'; import { ERROR_MESSAGE } from '../../constants'; import { SUCCESS_MESSAGE } from '../../constants/message'; @@ -29,3 +33,10 @@ export const usePostArticlesMutation = () => { }, }); }; + +export const usePutArticleBookmarkMutation = () => { + return useMutation(requestPutArticleBookmark, { + onSuccess: () => {}, + onError: () => {}, + }); +}; diff --git a/frontend/src/mocks/db/articles.json b/frontend/src/mocks/db/articles.json index 9105fbb0a..cac0d4f16 100644 --- a/frontend/src/mocks/db/articles.json +++ b/frontend/src/mocks/db/articles.json @@ -14,5 +14,21 @@ "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": 3, + "userName": "패트릭", + "title": "CORS", + "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": 4, + "userName": "패트릭", + "title": "CORS", + "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 c9b74676e..9419615fb 100644 --- a/frontend/src/mocks/handlers/articles.ts +++ b/frontend/src/mocks/handlers/articles.ts @@ -31,4 +31,8 @@ export const articlesHandler = [ articles.push(newArticle); return res(ctx.status(200), ctx.json(newArticle)); }), + + rest.put(`${BASE_URL}/articles/:articleId/bookmark`, (req, res, ctx) => { + return res(ctx.status(200)); + }), ]; diff --git a/frontend/src/models/Article.ts b/frontend/src/models/Article.ts index 742bcfd9b..1f194f7a5 100644 --- a/frontend/src/models/Article.ts +++ b/frontend/src/models/Article.ts @@ -21,3 +21,8 @@ export interface MetaOgResponse { imageUrl: string; title: string; } + +export interface ArticleBookmarkPutRequest { + articleId: number; + bookmark: boolean; +} From 144006655214c5a133cdf765455cfe420def1c7d Mon Sep 17 00:00:00 2001 From: hae-on <solbi2004@naver.com> Date: Fri, 15 Sep 2023 18:08:48 +0900 Subject: [PATCH 04/16] =?UTF-8?q?feat:=20=ED=95=84=ED=84=B0=EB=A7=81?= =?UTF-8?q?=EA=B3=BC=20=EC=97=B0=EA=B4=80=EB=90=9C=20=EB=B6=81=EB=A7=88?= =?UTF-8?q?=ED=81=AC=20=EA=B8=B0=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/apis/articles.ts | 5 ++++ .../Article/ArticleBookmarkFIlter.tsx | 28 +++++++++++++++++++ .../src/components/Article/ArticleList.tsx | 11 ++++---- frontend/src/hooks/queries/article.ts | 14 +++++----- frontend/src/mocks/db/articles.json | 4 +++ frontend/src/mocks/handlers/articles.ts | 10 +++++++ frontend/src/models/Article.ts | 5 ++++ frontend/src/pages/ArticleListPage/index.tsx | 27 ++++++++++++++---- 8 files changed, 85 insertions(+), 19 deletions(-) create mode 100644 frontend/src/components/Article/ArticleBookmarkFIlter.tsx diff --git a/frontend/src/apis/articles.ts b/frontend/src/apis/articles.ts index 6982ceddb..4f4f8bd46 100644 --- a/frontend/src/apis/articles.ts +++ b/frontend/src/apis/articles.ts @@ -2,6 +2,7 @@ import { client } from '.'; import { ArticleBookmarkPutRequest, ArticleRequest, + CourseFilter, MetaOgRequest, MetaOgResponse, } from '../models/Article'; @@ -17,3 +18,7 @@ export const requestGetMetaOg = ({ url }: MetaOgRequest) => { export const requestPutArticleBookmark = ({ articleId, bookmark }: ArticleBookmarkPutRequest) => { return client.put(`/articles/${articleId}/bookmark`, { checked: bookmark }); }; + +export const requestGetFilteredArticle = (course: string, bookmark: boolean) => { + return client.get(`/articles/filter?course=${course}&bookmark=${bookmark}`); +}; diff --git a/frontend/src/components/Article/ArticleBookmarkFIlter.tsx b/frontend/src/components/Article/ArticleBookmarkFIlter.tsx new file mode 100644 index 000000000..82a451f71 --- /dev/null +++ b/frontend/src/components/Article/ArticleBookmarkFIlter.tsx @@ -0,0 +1,28 @@ +import styled from '@emotion/styled'; + +interface ArticleBookmarkFilterProps { + checked: boolean; + handleCheckBookmark: React.ChangeEventHandler<HTMLInputElement>; +} + +const ArticleBookmarkFilter = ({ checked, handleCheckBookmark }: ArticleBookmarkFilterProps) => { + return ( + <ArticleBookmarkFilterContainer> + <input + type="checkbox" + checked={checked} + onChange={handleCheckBookmark} + value="북마크한 아티클" + /> + </ArticleBookmarkFilterContainer> + ); +}; + +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 <div>loading...</div>; - if (isError) return <div>error...</div>; +interface ArticleListProps { + articles: ArticleType[]; +} +const ArticleList = ({ articles }: ArticleListProps) => { return ( <Styled.Container> {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<ArticleType[]>([QUERY_KEY.articles], async () => { - const response = await requestGetArticles(); +export const useGetFilteredArticleQuery = (course: string, bookmark: boolean) => { + return useQuery<ArticleType[]>([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<CategoryOptions>(CATEGORY_OPTIONS[0]); + const [selectedCourse, setSelectedCourse] = useState<CategoryOptions>(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<HTMLInputElement> = (e) => { + setChecked(e.currentTarget.checked); + // getFilteredArticles(); }; return ( @@ -35,12 +49,13 @@ const ArticleListPage = () => { <SelectBoxWrapper> <SelectBox isClearable={false} - value={selectedOption} - defaultOption={selectedOption} + value={selectedCourse} + defaultOption={selectedCourse} options={CATEGORY_OPTIONS} onChange={changeFilterOption} /> </SelectBoxWrapper> + <ArticleBookmarkFilter checked={checked} handleCheckBookmark={handleCheckBookmark} /> <Button type="button" size="X_SMALL" @@ -52,7 +67,7 @@ const ArticleListPage = () => { 글쓰기 </Button> </Container> - <ArticleList /> + <ArticleList articles={filteredArticles} /> </div> ); }; From 694c7b1e669c7cca70642a7543d0aa5e6aaafc78 Mon Sep 17 00:00:00 2001 From: hae-on <solbi2004@naver.com> Date: Mon, 18 Sep 2023 15:53:38 +0900 Subject: [PATCH 05/16] =?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: 이도현 <Creative-Lee@users.noreply.github.com> --- 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 <solbi2004@naver.com> Date: Mon, 18 Sep 2023 15:54:43 +0900 Subject: [PATCH 06/16] =?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: 이도현 <Creative-Lee@users.noreply.github.com> --- 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<HTMLInputElement> = (e) => { setChecked(e.currentTarget.checked); - // getFilteredArticles(); }; + useEffect(() => { + getFilteredArticles(); + }, [checked, selectedCourse]); + return ( <div css={[MainContentStyle]}> <Container> - <SelectBoxWrapper> - <SelectBox - isClearable={false} - value={selectedCourse} - defaultOption={selectedCourse} - options={CATEGORY_OPTIONS} - onChange={changeFilterOption} - /> - </SelectBoxWrapper> - <ArticleBookmarkFilter checked={checked} handleCheckBookmark={handleCheckBookmark} /> + <FilteringWrapper> + <SelectBoxWrapper> + <SelectBox + isClearable={false} + value={selectedCourse} + defaultOption={selectedCourse} + options={CATEGORY_OPTIONS} + onChange={changeFilterOption} + /> + </SelectBoxWrapper> + <ArticleBookmarkFilter checked={checked} handleCheckBookmark={handleCheckBookmark} /> + </FilteringWrapper> <Button type="button" size="X_SMALL" @@ -82,6 +86,10 @@ export const Container = styled.div` gap: 15px; `; +const FilteringWrapper = styled.div` + display: flex; +`; + const SelectBoxWrapper = styled.div` width: 150px; `; From 4cdd4fb081d9b50d36cb3778e0eeb059cddb11ea Mon Sep 17 00:00:00 2001 From: hae-on <solbi2004@naver.com> Date: Mon, 18 Sep 2023 15:54:57 +0900 Subject: [PATCH 07/16] =?UTF-8?q?refactor:=20=EB=B6=81=EB=A7=88=ED=81=AC?= =?UTF-8?q?=20=EB=B2=84=ED=8A=BC=20=EB=94=94=EC=9E=90=EC=9D=B8=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: 이도현 <Creative-Lee@users.noreply.github.com> --- .../src/components/Article/Article.style.tsx | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/frontend/src/components/Article/Article.style.tsx b/frontend/src/components/Article/Article.style.tsx index 14e03d311..13980b9b4 100644 --- a/frontend/src/components/Article/Article.style.tsx +++ b/frontend/src/components/Article/Article.style.tsx @@ -72,19 +72,20 @@ export const Title = styled.p` -webkit-box-orient: vertical; `; -export const ArticleBookmarkButtonStyle = css` - flex-direction: column; - padding: 0; - width: fit-content; - font-size: 1.4rem; +export const BookmarkWrapper = styled.div` + width: 100%; + display: flex; + justify-content: flex-end; +`; +export const ArticleBookmarkButtonStyle = css` + width: initial; background-color: transparent; - color: ${COLOR.BLACK_800}; + text-align: right; & > img { - margin-right: 0; - width: 1.8rem; - height: 1.8rem; + width: 2.3rem; + height: 2.3rem; } `; From 1744871d5c1a0cc57cc06f9cf2f6da9eea93b0b4 Mon Sep 17 00:00:00 2001 From: hae-on <solbi2004@naver.com> Date: Mon, 18 Sep 2023 15:55:15 +0900 Subject: [PATCH 08/16] =?UTF-8?q?refactor:=20=EB=B6=81=EB=A7=88=ED=81=AC?= =?UTF-8?q?=20=EB=B2=84=ED=8A=BC=20wrapper?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: 이도현 <Creative-Lee@users.noreply.github.com> --- frontend/src/components/Article/Article.tsx | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/frontend/src/components/Article/Article.tsx b/frontend/src/components/Article/Article.tsx index 4211117b4..7b9069414 100644 --- a/frontend/src/components/Article/Article.tsx +++ b/frontend/src/components/Article/Article.tsx @@ -33,11 +33,13 @@ const Article = ({ id, title, userName, url, createdAt, imageUrl }: ArticleType) <Styled.CreatedAt>{createdAt.split(' ')[0]}</Styled.CreatedAt> </Styled.ArticleInfoWrapper> <Styled.Title>{title}</Styled.Title> - <Scrap - scrap={bookmark} - onClick={toggleBookmark} - cssProps={Styled.ArticleBookmarkButtonStyle} - /> + <Styled.BookmarkWrapper> + <Scrap + scrap={bookmark} + onClick={toggleBookmark} + cssProps={Styled.ArticleBookmarkButtonStyle} + /> + </Styled.BookmarkWrapper> </Styled.ArticleInfoContainer> </Styled.Anchor> </Styled.Container> From 3291400c4b4475bc767300feef087886e5a7d2b2 Mon Sep 17 00:00:00 2001 From: hae-on <solbi2004@naver.com> Date: Mon, 18 Sep 2023 15:55:30 +0900 Subject: [PATCH 09/16] =?UTF-8?q?refactor:=20=EB=B6=81=EB=A7=88=ED=81=AC?= =?UTF-8?q?=20=EB=B2=84=ED=8A=BC=20label=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: 이도현 <Creative-Lee@users.noreply.github.com> --- .../Article/ArticleBookmarkFIlter.tsx | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/frontend/src/components/Article/ArticleBookmarkFIlter.tsx b/frontend/src/components/Article/ArticleBookmarkFIlter.tsx index 82a451f71..9fb48f7d9 100644 --- a/frontend/src/components/Article/ArticleBookmarkFIlter.tsx +++ b/frontend/src/components/Article/ArticleBookmarkFIlter.tsx @@ -8,12 +8,10 @@ interface ArticleBookmarkFilterProps { const ArticleBookmarkFilter = ({ checked, handleCheckBookmark }: ArticleBookmarkFilterProps) => { return ( <ArticleBookmarkFilterContainer> - <input - type="checkbox" - checked={checked} - onChange={handleCheckBookmark} - value="북마크한 아티클" - /> + <label> + <input type="checkbox" checked={checked} onChange={handleCheckBookmark} /> + 북마크한 아티클 + </label> </ArticleBookmarkFilterContainer> ); }; @@ -21,8 +19,10 @@ const ArticleBookmarkFilter = ({ checked, handleCheckBookmark }: ArticleBookmark export default ArticleBookmarkFilter; const ArticleBookmarkFilterContainer = styled.div` - width: 100px; - border: 1px solid black; - border-radius: 5px; + display: flex; + align-items: center; + margin-left: 10px; + width: 150px; + height: 100%; font-size: 1.5rem; `; From fce77c66cd3f8aa8dc909fc50fa32d903263bc2b Mon Sep 17 00:00:00 2001 From: hae-on <solbi2004@naver.com> Date: Wed, 20 Sep 2023 16:49:59 +0900 Subject: [PATCH 10/16] =?UTF-8?q?docs:=20=EA=B8=B0=EC=A1=B4=20=EC=BD=94?= =?UTF-8?q?=EB=93=9C=20=EB=AC=B8=EC=A0=9C=EC=97=90=20=EA=B4=80=ED=95=9C=20?= =?UTF-8?q?=EC=A3=BC=EC=84=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/components/Controls/SelectBox.tsx | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/frontend/src/components/Controls/SelectBox.tsx b/frontend/src/components/Controls/SelectBox.tsx index 4e73a86c2..168875962 100644 --- a/frontend/src/components/Controls/SelectBox.tsx +++ b/frontend/src/components/Controls/SelectBox.tsx @@ -34,6 +34,25 @@ interface SelectOption { label: string; } +/** + FIXME: value props type SelectOption['value'] 로 변경되어야 함. + 아래 예시처럼 type을 좁힐 수 없는 문제가 있음. + + const CATEGORY_OPTIONS = [ + { value: '', label: '전체보기' }, + { value: 'frontend', label: '프론트엔드' }, + { value: 'backend', label: '백엔드' }, + { value: 'android', label: '안드로이드' }, + ]; + + type CategoryOptions = typeof CATEGORY_OPTIONS[number]; + + ->type CategoryOptions = { + value: string; + label: string; + } + 위 처럼 value type을 좁힐 수 없음. + */ interface SelectBoxProps { isMulti?: boolean; options: SelectOption[]; From 7a9584883e90ec37cdfb696742a866091a93f8f7 Mon Sep 17 00:00:00 2001 From: hae-on <solbi2004@naver.com> Date: Wed, 20 Sep 2023 16:50:32 +0900 Subject: [PATCH 11/16] =?UTF-8?q?refactor:=20bookmark=20=EC=B4=88=EA=B8=B0?= =?UTF-8?q?=EA=B0=92=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/components/Article/Article.tsx | 4 ++-- frontend/src/models/Article.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/frontend/src/components/Article/Article.tsx b/frontend/src/components/Article/Article.tsx index 7b9069414..0d55d5f71 100644 --- a/frontend/src/components/Article/Article.tsx +++ b/frontend/src/components/Article/Article.tsx @@ -5,9 +5,9 @@ import { useRef, useState } from 'react'; import { usePutArticleBookmarkMutation } from '../../hooks/queries/article'; import debounce from '../../utils/debounce'; -const Article = ({ id, title, userName, url, createdAt, imageUrl }: ArticleType) => { +const Article = ({ id, title, userName, url, createdAt, imageUrl, isBookMarked }: ArticleType) => { const bookmarkRef = useRef(false); - const [bookmark, setBookmark] = useState(false); + const [bookmark, setBookmark] = useState(isBookMarked); const { mutate: putBookmark } = usePutArticleBookmarkMutation(); const toggleBookmark: React.MouseEventHandler<HTMLButtonElement> = (e) => { diff --git a/frontend/src/models/Article.ts b/frontend/src/models/Article.ts index 4bce9855f..dc7540160 100644 --- a/frontend/src/models/Article.ts +++ b/frontend/src/models/Article.ts @@ -8,7 +8,7 @@ export interface ArticleType { id: number; userName: string; title: string; - isBookMarked: false; + isBookMarked: boolean; url: string; createdAt: string; imageUrl: string; From f566aaa389fe48f0bc9e1876b336815e527b6791 Mon Sep 17 00:00:00 2001 From: hae-on <solbi2004@naver.com> Date: Wed, 20 Sep 2023 16:50:59 +0900 Subject: [PATCH 12/16] =?UTF-8?q?chore:=20url=EC=97=90=20filter=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/mocks/handlers/articles.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/mocks/handlers/articles.ts b/frontend/src/mocks/handlers/articles.ts index 441dae3e9..2302996cc 100644 --- a/frontend/src/mocks/handlers/articles.ts +++ b/frontend/src/mocks/handlers/articles.ts @@ -37,7 +37,7 @@ export const articlesHandler = [ return res(ctx.status(200)); }), - rest.get(`${BASE_URL}/articles/filter`, (req, res, ctx) => { + rest.get(`${BASE_URL}/articles/`, (req, res, ctx) => { const course = req.url.searchParams.get('course'); const onlyBookmarked = req.url.searchParams.get('onlyBookmarked'); From 974c165ec4c9fed4667f4d144c254af8a5628054 Mon Sep 17 00:00:00 2001 From: hae-on <solbi2004@naver.com> Date: Wed, 20 Sep 2023 16:53:22 +0900 Subject: [PATCH 13/16] =?UTF-8?q?refactor:=20url=20filter=20=EC=82=AD?= =?UTF-8?q?=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/apis/articles.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/apis/articles.ts b/frontend/src/apis/articles.ts index b2f1bace5..e3f223ea0 100644 --- a/frontend/src/apis/articles.ts +++ b/frontend/src/apis/articles.ts @@ -19,5 +19,5 @@ export const requestPutArticleBookmark = ({ articleId, bookmark }: ArticleBookma }; export const requestGetFilteredArticle = (course: string, bookmark: boolean) => { - return client.get(`/articles/filter?course=${course}&onlyBookmarked=${bookmark}`); + return client.get(`/articles?course=${course}&onlyBookmarked=${bookmark}`); }; From cd493ba09205d00866c567aadb3ce471f18d81fe Mon Sep 17 00:00:00 2001 From: hae-on <solbi2004@naver.com> Date: Wed, 20 Sep 2023 16:53:41 +0900 Subject: [PATCH 14/16] =?UTF-8?q?refactor:=20select=20value=20=EC=98=81?= =?UTF-8?q?=EC=96=B4=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 --- frontend/src/pages/ArticleListPage/index.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/frontend/src/pages/ArticleListPage/index.tsx b/frontend/src/pages/ArticleListPage/index.tsx index bc0ec7f49..91bd8aacf 100644 --- a/frontend/src/pages/ArticleListPage/index.tsx +++ b/frontend/src/pages/ArticleListPage/index.tsx @@ -14,10 +14,10 @@ import ArticleBookmarkFilter from '../../components/Article/ArticleBookmarkFIlte import { useGetFilteredArticleQuery } from '../../hooks/queries/article'; const CATEGORY_OPTIONS = [ - { value: '전체보기', label: '전체보기' }, - { value: '프론트엔드', label: '프론트엔드' }, - { value: '백엔드', label: '백엔드' }, - { value: '안드로이드', label: '안드로이드' }, + { value: 'all', label: '전체보기' }, + { value: 'frontend', label: '프론트엔드' }, + { value: 'backend', label: '백엔드' }, + { value: 'android', label: '안드로이드' }, ]; type CategoryOptions = typeof CATEGORY_OPTIONS[number]; From 19dcb2fb23f31fdba035329abf6ddac27c1ad8d6 Mon Sep 17 00:00:00 2001 From: hae-on <solbi2004@naver.com> Date: Wed, 20 Sep 2023 16:58:06 +0900 Subject: [PATCH 15/16] =?UTF-8?q?feat:=20=EB=A1=9C=EA=B7=B8=EC=9D=B8?= =?UTF-8?q?=ED=95=9C=20=EC=9C=A0=EC=A0=80=EC=97=90=EA=B2=8C=EB=A7=8C=20?= =?UTF-8?q?=EB=B2=84=ED=8A=BC=EA=B3=BC=20=EB=B6=81=EB=A7=88=ED=81=AC=20?= =?UTF-8?q?=ED=95=84=ED=84=B0=20=EB=B3=B4=EC=9D=B4=EB=8F=84=EB=A1=9D=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/pages/ArticleListPage/index.tsx | 32 ++++++++++++-------- 1 file changed, 20 insertions(+), 12 deletions(-) diff --git a/frontend/src/pages/ArticleListPage/index.tsx b/frontend/src/pages/ArticleListPage/index.tsx index 91bd8aacf..9f6882c0e 100644 --- a/frontend/src/pages/ArticleListPage/index.tsx +++ b/frontend/src/pages/ArticleListPage/index.tsx @@ -8,10 +8,11 @@ import { COLOR, PATH } from '../../constants'; import styled from '@emotion/styled'; import { MainContentStyle } from '../../PageRouter'; import SelectBox from '../../components/Controls/SelectBox'; -import { useEffect, useState } from 'react'; +import { useContext, useEffect, useState } from 'react'; import { css } from '@emotion/react'; import ArticleBookmarkFilter from '../../components/Article/ArticleBookmarkFIlter'; import { useGetFilteredArticleQuery } from '../../hooks/queries/article'; +import { UserContext } from '../../contexts/UserProvider'; const CATEGORY_OPTIONS = [ { value: 'all', label: '전체보기' }, @@ -28,6 +29,9 @@ const ArticleListPage = () => { const [selectedCourse, setSelectedCourse] = useState<CategoryOptions>(CATEGORY_OPTIONS[0]); const [checked, setChecked] = useState(false); + const { user } = useContext(UserContext); + const { isLoggedIn } = user; + const { data: filteredArticles = [], refetch: getFilteredArticles } = useGetFilteredArticleQuery( selectedCourse.value, checked @@ -58,18 +62,22 @@ const ArticleListPage = () => { onChange={changeFilterOption} /> </SelectBoxWrapper> - <ArticleBookmarkFilter checked={checked} handleCheckBookmark={handleCheckBookmark} /> + {isLoggedIn && ( + <ArticleBookmarkFilter checked={checked} handleCheckBookmark={handleCheckBookmark} /> + )} </FilteringWrapper> - <Button - type="button" - size="X_SMALL" - icon={PencilIcon} - alt="새 아티클 쓰기 아이콘" - onClick={goNewArticlePage} - cssProps={WriteButtonStyle} - > - 글쓰기 - </Button> + {isLoggedIn && ( + <Button + type="button" + size="X_SMALL" + icon={PencilIcon} + alt="새 아티클 쓰기 아이콘" + onClick={goNewArticlePage} + cssProps={WriteButtonStyle} + > + 글쓰기 + </Button> + )} </Container> <ArticleList articles={filteredArticles} /> </div> From d9501c1c4b430581cfc514aea0552043f449523b Mon Sep 17 00:00:00 2001 From: hae-on <solbi2004@naver.com> Date: Wed, 20 Sep 2023 17:43:19 +0900 Subject: [PATCH 16/16] =?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)); }),