Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[FE] 아티클 에디터 페이지를 제작한다. #993

Merged
merged 12 commits into from
Nov 30, 2024
Merged
1 change: 1 addition & 0 deletions frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
"@emotion/styled": "^11.11.5",
"@sentry/react": "^8.22.0",
"@tanstack/react-query": "^5.51.15",
"@uiw/react-markdown-editor": "^6.1.2",
"@uiw/react-markdown-preview": "^5.1.2",
"babel-loader": "^9.2.1",
"init": "^0.1.2",
Expand Down
7 changes: 6 additions & 1 deletion frontend/src/apis/article.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import fetcher from '@/apis/fetcher';
import { BASE_URL, ENDPOINT } from '@/apis/url';
import { Article } from '@/types/article';
import { Article, ArticlePostForm } from '@/types/article';

export const getArticleList = async () => {
const response = await fetcher.get({ url: BASE_URL + ENDPOINT.ARTICLES });
Expand All @@ -13,3 +13,8 @@ export const getArticle = async (id: number) => {
const data = await response.json();
return data as Article;
};

export const postArticle = async (article: ArticlePostForm) => {
const response = await fetcher.post({ url: BASE_URL + ENDPOINT.ARTICLES, body: article });
return response;
Comment on lines +17 to +19
Copy link
Contributor

@skiende74 skiende74 Nov 27, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

return할때 as 타입단언 해주면 좋을거같아요

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

앗 네넹 ~!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

앗 그런데 이부분이 post response 라....! 타입이 따로 없네요
res 를 리턴하지 말까요?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

맞네요 !

};
14 changes: 11 additions & 3 deletions frontend/src/components/ArticleDetail/ArticleContent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,17 @@ const ArticleContent = () => {
<S.Title>{article?.title}</S.Title>
<MarkdownPreview
source={article?.content}
style={{ fontSize: '1.6rem', lineHeight: '2.6rem', fontFamily: 'SUITE Variable' }}
wrapperElement={{
'data-color-mode': 'light',
style={{ padding: 10 }}
rehypeRewrite={(node, index, parent) => {
if (
node.type === 'element' &&
node.tagName === 'a' &&
parent &&
parent.type === 'element' &&
/^h(1|2|3|4|5|6)/.test(parent.tagName)
) {
parent.children = parent.children.slice(1);
}
}}
/>
</S.Wrapper>
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/components/_common/Button/Button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ const sizeStyles = {
min-width: 7rem;
`,
small: css`
padding: 1rem 1.6rem;
padding: 0.8rem 1.2rem;
${title3}
`,
medium: css`
Expand Down
21 changes: 21 additions & 0 deletions frontend/src/components/_common/layout/DesktopLayout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import styled from '@emotion/styled';

interface Props {
children: React.ReactNode;
}

const DesktopLayout = ({ children }: Props) => {
return <S.BodyWrapper>{children}</S.BodyWrapper>;
};

export default DesktopLayout;

const S = {
BodyWrapper: styled.div`
max-width: 120rem;
min-height: 100dvh;

margin: 0 auto;
box-sizing: border-box;
`,
};
27 changes: 27 additions & 0 deletions frontend/src/components/_common/layout/MobileLayout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import styled from '@emotion/styled';

import theme from '@/styles/theme';

interface Props {
children: React.ReactNode;
}

const MobileLayout = ({ children }: Props) => {
return <S.BodyWrapper>{children}</S.BodyWrapper>;
};

export default MobileLayout;

const S = {
BodyWrapper: styled.div`
max-width: 60rem;
min-height: 100dvh;

margin: 0 auto;
box-sizing: border-box;

border-left: 0.1rem solid ${theme.palette.grey200};
border-right: 0.1rem solid ${theme.palette.grey200};
box-shadow: 0 0 2rem ${theme.palette.grey100};
`,
};
2 changes: 2 additions & 0 deletions frontend/src/constants/routePath.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,6 @@ export const ROUTE_PATH = {
/* etc */
location: '/location',
myPage: '/my-page',
admin: '/admin',
articleEditor: '/isHaileyGod',
Comment on lines +25 to +26
Copy link
Contributor

@skiende74 skiende74 Nov 27, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

신이었다..

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋ

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋ

};
21 changes: 21 additions & 0 deletions frontend/src/hooks/query/usePostArticleQuery.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { useMutation } from '@tanstack/react-query';
import { useNavigate } from 'react-router-dom';

import { postArticle } from '@/apis/article';
import { ROUTE_PATH } from '@/constants/routePath';
import useToast from '@/hooks/useToast';

const usePostArticleQuery = () => {
const { showToast } = useToast();
const navigate = useNavigate();

return useMutation({
mutationFn: postArticle,
onSuccess: () => {
showToast({ message: '아티클 등록 완료!' });
navigate(ROUTE_PATH.admin);
},
});
};

export default usePostArticleQuery;
6 changes: 5 additions & 1 deletion frontend/src/mocks/handlers/article.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,11 @@ export const ArticleHandlers = [
return HttpResponse.json(article, { status: 200 });
}),

http.get(BASE_URL + ENDPOINT.ARTICLE_ID(2), () => {
http.get(BASE_URL + ENDPOINT.ARTICLE_ID(3), () => {
return HttpResponse.json(article, { status: 404 });
}),

http.post(BASE_URL + ENDPOINT.ARTICLES, () => {
return HttpResponse.json({}, { status: 200 });
}),
];
72 changes: 72 additions & 0 deletions frontend/src/pages/AdminPage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import styled from '@emotion/styled';
import { Link } from 'react-router-dom';

import { ROUTE_PATH } from '@/constants/routePath';
import { boxShadowSpread, flexRow, title2, title3 } from '@/styles/common';

const AdminPage = () => {
return (
<S.PageWrapper>
<S.QuestionBox>
<S.QuestionText>Is Hailey God?</S.QuestionText>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

여기도 갓있다ㅋㅋㅋㅋㅋ인죵

<S.ButtonWrapper>
<S.Button color={'red'}>No</S.Button>
<Link to={ROUTE_PATH.articleEditor}>
<S.Button>Yes</S.Button>
</Link>
</S.ButtonWrapper>
</S.QuestionBox>
</S.PageWrapper>
);
};

export default AdminPage;

const S = {
PageWrapper: styled.div`
display: flex;
justify-content: center;
align-items: center;
height: 100vh; /* 전체 화면 높이에 맞춰 정렬 */

background-color: ${({ theme }) => theme.palette.white};
`,

QuestionBox: styled.div`
padding: 2rem;

background-color: ${({ theme }) => theme.palette.white};

text-align: center;
border-radius: 10px;
${boxShadowSpread}
`,

QuestionText: styled.h2`
margin-bottom: 2rem;

color: ${({ theme }) => theme.palette.black};
${title2}
`,
ButtonWrapper: styled.div`
${flexRow}
justify-content: center;
gap: 1rem;
`,
Button: styled.button<{ color?: string }>`
padding: 0.8rem 2rem;
border: none;

background-color: ${({ theme, color }) => (color === 'red' ? theme.palette.red500 : theme.palette.green500)};

color: ${({ theme }) => theme.palette.white};
${title3}
border-radius: 5px;
cursor: pointer;
transition: background-color 0.3s;

&:hover {
background-color: ${({ theme, color }) => (color === 'red' ? theme.palette.red600 : theme.palette.green600)};
}
`,
};
127 changes: 127 additions & 0 deletions frontend/src/pages/ArticleEditorPage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
import styled from '@emotion/styled';
import MarkdownEditor from '@uiw/react-markdown-editor';
import { useState } from 'react';

import Button from '@/components/_common/Button/Button';
import FormField from '@/components/_common/FormField/FormField'; // 예시로 주신 FormField 컴포넌트 사용
import DesktopLayout from '@/components/_common/layout/DesktopLayout';
import usePostArticleQuery from '@/hooks/query/usePostArticleQuery';
import { flexColumn, flexRow, flexSpaceBetween, title2 } from '@/styles/common';

const ArticleEditorPage = () => {
const [title, setTitle] = useState('');
const [keyword, setKeyword] = useState('');
const [summary, setSummary] = useState('');
const [thumbnail] = useState('');
const [content, setContent] = useState('# 여기에 아티클을 작성해주세요');

const { mutate: addArticle } = usePostArticleQuery();

const handleSubmit = () => {
addArticle({
title,
content,
keyword,
thumbnail,
summary,
});
};

return (
<>
<S.Header>
<S.HeaderContents>
<S.Title>방끗 Article Editor</S.Title>

{/* 저장 버튼 */}
<Button type="submit" label="저장" isSquare color="dark" onClick={handleSubmit} size="small" />
</S.HeaderContents>
</S.Header>
<DesktopLayout>
<S.Form
onSubmit={e => {
e.preventDefault();
handleSubmit();
}}
>
{/* 제목 폼 필드 */}
<FormField>
<FormField.Label label="아티클 제목" htmlFor="title" required />
<FormField.Input
placeholder="제목을 입력하세요"
onChange={e => setTitle(e.target.value)}
name="title"
id="title"
value={title}
/>
</FormField>

{/* 키워드 폼 필드 */}
<FormField>
<FormField.Label label="키워드" htmlFor="keyword" required />
<FormField.Input
placeholder="키워드를 입력하세요"
onChange={e => setKeyword(e.target.value)}
name="keyword"
id="keyword"
value={keyword}
/>
</FormField>

{/* 요약 폼 필드 */}
<FormField>
<FormField.Label label="요약" htmlFor="summary" required />
<FormField.Input
placeholder="요약을 입력하세요"
onChange={e => setSummary(e.target.value)}
name="summary"
id="summary"
value={summary}
/>
</FormField>

{/* MarkdownEditor로 content 작성 */}
<div style={{ marginBottom: '20px' }}>
<MarkdownEditor
value={content}
height="70vh"
onChange={value => setContent(value)}
visible={true}
enablePreview={true}
/>
</div>
</S.Form>
</DesktopLayout>
</>
);
};

export default ArticleEditorPage;

const S = {
Header: styled.header`
${flexRow}
justify-content: center;

width: 100vw;
height: 50px;
border-bottom: 1px solid ${({ theme }) => theme.palette.grey200};

box-sizing: border-box;

margin-bottom: 20px;
`,
HeaderContents: styled.div`
width: 120rem;
${flexRow}
${flexSpaceBetween}
align-items: center;
`,
Title: styled.h1`
${title2}
`,
Form: styled.form`
${flexColumn}
gap: 20px;
`,
};
Loading