{article.writer &&
{article.writer.nickname}
}
{getArticleTime(article.createdAt)}
diff --git a/src/Components/PreviewArticleNoti.jsx b/src/Components/PreviewArticleNoti.jsx
index 3f8ab80..e5cfb01 100644
--- a/src/Components/PreviewArticleNoti.jsx
+++ b/src/Components/PreviewArticleNoti.jsx
@@ -1,13 +1,8 @@
-import dayjs from 'dayjs';
+import { getArticleTime } from 'Utils/dayjsUtils';
import Styled from './PreviewArticle.styled';
const PreviewArticleNoti = ({ article, onClickArticle }) => {
- const getArticleTime = time =>
- dayjs(time).isSame(dayjs(), 'day')
- ? dayjs(time).format('HH:mm')
- : dayjs(time).format('MM/DD');
-
return (
{
return `${API.url('/articles')}${path}`;
@@ -91,6 +92,25 @@ const ArticleService = {
* `200` : success \
* `401` : fail
*/
+ getAllArticles: async (categoryId, page) => {
+ const method = 'GET';
+ const url = articleUrl('');
+ const take = 1000;
+ // const take = 3;
+ const params = { categoryId, page, take };
+
+ let response;
+ try {
+ response = await API.AXIOS({
+ params,
+ method,
+ url,
+ });
+ } catch (error) {
+ alert(error);
+ }
+ return response.data;
+ },
getArticles: async (categoryId, page) => {
const method = 'GET';
const url = articleUrl('');
@@ -145,21 +165,23 @@ const ArticleService = {
* `200` : success
* `401` : fail
*/
- getArticlesById: async articlesId => {
+ getArticleById: async articleId => {
const method = 'GET';
- const url = articleUrl(`/${articlesId}`);
+ const url = articleUrl(`/${articleId}`);
- let response;
- try {
- response = await API.AXIOS({
- method,
- url,
- });
- } catch (error) {
- alert(error);
- }
+ const response = await API.AXIOS({
+ method,
+ url,
+ });
return response.data;
},
+ // useArticle(articleId) {
+ // return useQuery(
+ // ['getArticleById', articleId],
+ // async () => await this.getArticleById(articleId),
+ // { suspense: true },
+ // );
+ // },
/**
* **UPDATE** One Articles By Articles ID
* @param {string} articlesId
@@ -196,15 +218,10 @@ const ArticleService = {
const method = 'DELETE';
const url = articleUrl(`/${articlesId}`);
- let response;
- try {
- response = await API.AXIOS({
- method,
- url,
- });
- } catch (error) {
- alert(error);
- }
+ const response = await API.AXIOS({
+ method,
+ url,
+ });
return response.data;
},
/**
@@ -238,18 +255,25 @@ const ArticleService = {
const url = articleUrl(`/${articlesId}/comments`);
const params = { order, page, take };
- let response;
- try {
- response = await API.AXIOS({
- method,
- url,
- params,
- });
- } catch (error) {
- alert(error);
- }
+ const response = await API.AXIOS({
+ method,
+ url,
+ params,
+ });
return response.data;
},
+ // useComments(articleId, order, page, take) {
+ // return useQuery(
+ // ['getCommentsById'],
+ // async () =>
+ // await ArticleService.getArticlesCommentsById(
+ // articleId,
+ // order,
+ // page,
+ // take,
+ // ),
+ // );
+ // },
editArticles: async (articlesId, articles) => {
const method = 'PUT';
const url = articleUrl(`/${articlesId}`);
diff --git a/src/Network/ArticleService_old.js b/src/Network/ArticleService_old.js
deleted file mode 100644
index 8a162af..0000000
--- a/src/Network/ArticleService_old.js
+++ /dev/null
@@ -1,93 +0,0 @@
-// # 게시글 /articles
-import { Article, Comment } from '../Entities';
-import * as API from './APIType';
-
-const authUrl = path => {
- return `${API.url('/artiles')}path`;
-};
-
-// - 가져오기
-// - 카테고리별 게시글 목록 GET /articles?category=”anonymous”
-// - 게시글 상세 GET /articles/:id
-// - 조회수 갱신까지 같이 한다. <❗️추후 수정 여지 있음❗️>
-// - 댓글 가져오기 GET /articles/:id/comments
-// - 추가하기 POST /articles
-// - 수정하기
-// - 본문 / 제목 수정하기 PUT /articles/:id
-// - 지우기 DELETE /articles/:id
-
-const generateRandomArticle = () => {
- const id = 1;
- const category_id = 1;
- const writer_id = 1;
- const title = 'this is title';
- const content = 'this is content';
- const view_count = 1;
- const comment_count = 2;
- const liked_count = 3;
-
- return new Article(
- id,
- category_id,
- writer_id,
- title,
- content,
- view_count,
- comment_count,
- liked_count,
- );
-};
-
-const generateRandomComment = () => {
- const id = 1;
- const article_id = 1;
- const writer_id = 1;
- const content = 'this is comment';
-
- return new Comment(id, article_id, writer_id, content);
-};
-
-const ArticleService = {
- fetchAllArticle: category_id => {
- return [
- generateRandomArticle(),
- generateRandomArticle(),
- generateRandomArticle(),
- generateRandomArticle(),
- generateRandomArticle(),
- generateRandomArticle(),
- generateRandomArticle(),
- generateRandomArticle(),
- generateRandomArticle(),
- generateRandomArticle(),
- generateRandomArticle(),
- generateRandomArticle(),
- generateRandomArticle(),
- generateRandomArticle(),
- generateRandomArticle(),
- generateRandomArticle(),
- generateRandomArticle(),
- ];
- },
- fetchArticle: id => {
- return generateRandomArticle();
- },
- fetchArticleComments: id => {
- return [
- generateRandomComment(),
- generateRandomComment(),
- generateRandomComment(),
- ];
- },
- createArticle: (title, content) => {
- return true;
- },
- updateArticle: (id, title, content) => {
- return true;
- },
- deleteArticle: id => {
- return true;
- },
-};
-
-export default ArticleService;
diff --git a/src/Network/CommentService_old.js b/src/Network/CommentService_old.js
deleted file mode 100644
index 89fd53a..0000000
--- a/src/Network/CommentService_old.js
+++ /dev/null
@@ -1,28 +0,0 @@
-// # 댓글 /comments
-
-// - 쓰기 POST /comments/:id
-// - 수정하기 PUT /comments/:id
-// - 삭제하기 DELETE /comments/:id
-
-const generateRandomComment = () => {
- const id = 1;
- const article_id = 1;
- const writer_id = 1;
- const content = "this is comment";
-
- return new Comment(id, article_id, writer_id, content);
-};
-
-const CommentService = {
- createComment: (article_id, writer_id, content) => {
- return generateRandomComment();
- },
- updateComment: (id) => {
- return true;
- },
- deleteComment: (id) => {
- return true;
- },
-};
-
-export default CommentService;
diff --git a/src/Network/ReactionService.js b/src/Network/ReactionService.js
index 27f22be..9d4264a 100644
--- a/src/Network/ReactionService.js
+++ b/src/Network/ReactionService.js
@@ -13,15 +13,10 @@ const ReactionService = {
createArticleReactionHeart: async id => {
const method = 'POST';
const url = reactionUrl(`/${id}`);
- let response;
- try {
- response = await API.AXIOS({
- method,
- url,
- });
- } catch (error) {
- console.log('error');
- }
+ const response = await API.AXIOS({
+ method,
+ url,
+ });
return response.data;
},
@@ -34,15 +29,10 @@ const ReactionService = {
createCommentReactionHeart: async (articleId, commentId) => {
const method = 'POST';
const url = reactionUrl(`/${articleId}/comments/${commentId}`);
- let response;
- try {
- response = await API.AXIOS({
- method,
- url,
- });
- } catch (error) {
- console.log('error');
- }
+ const response = await API.AXIOS({
+ method,
+ url,
+ });
return response.data;
},
};
diff --git a/src/Network/UserService_old.js b/src/Network/UserService_old.js
deleted file mode 100644
index bc482d5..0000000
--- a/src/Network/UserService_old.js
+++ /dev/null
@@ -1,94 +0,0 @@
-import { User, Notification } from '../Entities';
-import * as API from './APIType';
-// # 유저 /users
-
-// - 인증
-// - 로그아웃 DELETE /users/logout
-// - 깃헙 로그인 인증 POST /users/github
-// - 유저 42 인증 요청
-// - 42 로그인으로 인증 요청 POST /users/42auth/login
-// - 42 이메일로 인증 요청 POST /users/42auth/email
-// - 회원 탈퇴 DELETE /users
-// - 정보
-// - 로그인 및 내 정보 가져오기 GET /users
-// - 프로필 정보 수정 PUT /users/profile
-// - 닉네임 중복확인 GET /users/nickname
-// - 알람
-// - 가져오기 GET /users/alarm
-// - 읽음 PUT /users/alarm/readall <❗️추후 수정 여지 있음❗️>
-
-const generateRandomUser = () => {
- const id = 1;
- const nickname = 'ycha';
- const is_authenticated = true;
- const role = 'CADET';
- const charactor = 'https://picsum.photos/200/200';
-
- return new User(id, nickname, is_authenticated, role, charactor);
-};
-
-const generateRandomNotification = () => {
- const id = 1;
- const user_id = 'ycha';
- const type = 'NEW_COMMENT';
- const content = 'asdf';
- const time = new Date();
- const is_read = true;
-
- return new Notification(id, user_id, type, content, time, is_read);
-};
-
-const UserService = {
- Auth: {
- signOut: () => {},
- validateGithub: token => {
- return true;
- },
- validate42Login: intra_token => {
- return true;
- },
- validate42Email: intra_id => {
- return true;
- },
- deleteUser: () => {},
- },
- Info: {
- fetchUser: async () => {
- try {
- const method = 'GET';
- const headers = {};
- const body = {};
- const url = API.url('path');
- const response = await API.AXIOS({
- method,
- headers,
- body,
- url,
- });
- } catch (error) {
- console.log('error');
- }
- return generateRandomUser();
- },
- updateUserProfile: (character, nickname) => {
- return true;
- },
- checkDuplicateNickname: nickname => {
- return true;
- },
- },
- Noti: {
- fetchNotification: () => {
- return [
- generateRandomNotification(),
- generateRandomNotification(),
- generateRandomNotification(),
- ];
- },
- updateNotification: () => {
- return true;
- },
- },
-};
-
-export default UserService;
diff --git a/src/Pages/AlarmPage/Components/AlarmBody.jsx b/src/Pages/AlarmPage/Components/AlarmBody.jsx
index a25ddb4..1774a34 100644
--- a/src/Pages/AlarmPage/Components/AlarmBody.jsx
+++ b/src/Pages/AlarmPage/Components/AlarmBody.jsx
@@ -2,7 +2,7 @@ import { useState, useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import NotificationService from 'Network/NotificationService';
-import dayjs from 'dayjs';
+import { getArticleTime } from 'Utils/dayjsUtils';
import Styled from './AlarmArticle.styled.js';
@@ -26,8 +26,7 @@ const AlarmBody = () => {
const mainTextLen = 10;
const moveArticles = articleId => {
- alert('구현 중입니다!');
- // navi(`/article/${articleId}`);
+ navi(`/article/${articleId}`);
};
const previewMainText = article => {
@@ -38,8 +37,6 @@ const AlarmBody = () => {
return alarmType(context, article.type);
};
- const getArticleTime = time => dayjs(time).format('MM/DD HH:mm');
-
const readAlarm = async () => {
const read = await NotificationService.readAllNotifications();
};
@@ -68,7 +65,7 @@ const AlarmBody = () => {
className="article"
isRead={article.isRead}
isNotice={false}
- onClick={() => moveArticles(article.userId)}
+ onClick={() => moveArticles(article.articleId)}
>
새 댓글
{previewMainText(article)}
diff --git a/src/Pages/ArticlePage/ArticlePage.jsx b/src/Pages/ArticlePage/ArticlePage.jsx
index 95760d9..02b5d15 100644
--- a/src/Pages/ArticlePage/ArticlePage.jsx
+++ b/src/Pages/ArticlePage/ArticlePage.jsx
@@ -1,20 +1,32 @@
+import { Suspense, useContext } from 'react';
+import { ErrorBoundary } from 'react-error-boundary';
import { useParams } from 'react-router-dom';
import { Header } from 'Components';
-import { Body } from './Components';
+import { Article } from './Components';
+import { Comments } from './Components';
+import { Loading } from 'Components';
import Styled from './ArticlePage.styled';
+import { ErrorPage } from 'Pages';
+import { AuthContext } from 'App';
+import CreateComment from './Components/CreateComment';
const ArticlePage = () => {
+ const { curUser } = useContext(AuthContext);
const { id } = useParams();
return (
- <>
+ }>
-
-
-
- >
+ }>
+
+
+
+
+
+
+
);
};
diff --git a/src/Pages/ArticlePage/ArticlePage.styled.js b/src/Pages/ArticlePage/ArticlePage.styled.js
index 25b68c0..e98c652 100644
--- a/src/Pages/ArticlePage/ArticlePage.styled.js
+++ b/src/Pages/ArticlePage/ArticlePage.styled.js
@@ -1,13 +1,176 @@
import styled, { css } from 'styled-components';
import GlobalStyled from 'Styled/Global.styled';
-const ProfileImage = styled.img`
- ${props => (props.width ? `width: ${props.width};` : 'width: 2.5rem;')}
- ${props =>
- props.width ? `min-width: ${props.width};` : 'min-width: 2.5rem;'}
- ${props => (props.width ? `height: ${props.width};` : 'height: 2.5rem;')}
- border-radius: 10%;
- border: 0px;
+const ArticlePageDiv = styled.div`
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+
+ .comment_list_div {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ width: 100%;
+ }
+ }
+`;
+
+/// Article
+const ArticleViewDiv = styled.div`
+ display: flex;
+ flex-direction: column;
+ width: 100%;
+
+ .content_top {
+ display: flex;
+ flex-direction: row;
+ align-items: flex-end;
+ padding: 0.7rem;
+ width: 100%;
+ border-bottom: 1px solid ${GlobalStyled.theme.borderColor};
+
+ .title {
+ display: flex;
+ flex-direction: column;
+ width: 100%;
+
+ h1 {
+ font-size: 1rem;
+ font-weight: 600;
+ color: ${GlobalStyled.theme.textColor};
+ margin-bottom: 0.2rem;
+ padding-right: 0.5rem;
+ }
+
+ .info {
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+
+ h2 {
+ color: ${GlobalStyled.theme.textColorGray};
+ font-size: 0.7rem;
+ font-weight: 300;
+ margin-right: 0.8rem;
+ }
+
+ .edit_article {
+ display: flex;
+ flex-direction: row;
+ align-items: flex-end;
+ margin: 0 0.5rem;
+ margin-left: auto;
+
+ button {
+ padding: 0;
+ border: none;
+ width: max-content;
+ background: none;
+ cursor: pointer;
+ margin: 0 0.3rem;
+ font-size: 0.7rem;
+ font-weight: 400;
+ }
+ }
+ }
+ }
+ }
+
+ .toastui-editor-contents {
+ padding: 0.7rem;
+ }
+`;
+
+const ArticleReactionDiv = styled.div`
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ flex-direction: row;
+ margin-top: 1em;
+ margin-bottom: 0.2rem;
+ font-size: 0.95rem;
+ color: ${GlobalStyled.theme.likedCountColor};
+
+ svg {
+ width: 2rem;
+ height: 2rem;
+ margin-right: 0.2rem;
+ cursor: pointer;
+ }
+
+ &::after {
+ content: '${props => {
+ if (props.likedCount > 0) return props.likedCount;
+ else return '';
+ }}';
+ }
+`;
+///
+
+/// Comments
+const CommentsCountDiv = styled.div`
+ display: flex;
+ flex-direction: row;
+ width: 100%;
+ align-items: center;
+ justify-content: flex-start;
+ color: ${GlobalStyled.theme.commentIconColor};
+ font-size: 0.85rem;
+
+ margin-top: 0.5rem;
+ padding: 0.3rem 0.8rem 0.5rem 0.8rem;
+ border-top: 1.5px solid ${GlobalStyled.theme.borderColor};
+
+ svg {
+ width: 1.3rem;
+ height: 1.3rem;
+ margin-top: 0.2rem;
+ margin-right: 0.2rem;
+ }
+
+ &::after {
+ content: '${props => {
+ if (props.commentCount > 0) return props.commentCount;
+ else return '';
+ }}';
+ }
+`;
+
+const CommentViewDiv = styled.div`
+ border-top: 1px solid #e6e6e6;
+ padding: 0.5rem 0.8rem;
+ width: 100%;
+ display: flex;
+ flex-direction: column;
+
+ .info {
+ margin-bottom: 0.4rem;
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+ ${props => props.isMine && `flex-direction: row-reverse;`}
+
+ .text {
+ margin: 0rem 0.7rem;
+ ${props => props.isMine && `text-align: right;`}
+ h1 {
+ color: ${GlobalStyled.theme.textColor};
+ font-size: 0.9rem;
+ font-weight: 600;
+ line-height: 1.1;
+ }
+ h2 {
+ color: ${GlobalStyled.theme.textColorGray};
+ font-size: 0.4rem;
+ font-weight: 400;
+ }
+ }
+ }
+
+ .text {
+ margin-left: 0.2rem;
+ word-break: break-all;
+ }
`;
const CommentContent = styled.div`
@@ -75,8 +238,10 @@ const CommentContent = styled.div`
}
}
`;
+///
-const CreateCommentDiv = styled.div`
+/// CreateComment
+const CreateCommentViewDiv = styled.div`
position: sticky;
bottom: 1rem;
display: flex;
@@ -135,183 +300,24 @@ const CreateCommentDiv = styled.div`
}
`;
-const ArticleLikedDiv = styled.div`
- display: flex;
- align-items: center;
- justify-content: center;
- flex-direction: row;
- margin-top: 1em;
- margin-bottom: 0.2rem;
- font-size: 0.95rem;
- color: ${GlobalStyled.theme.buttonRed1};
-
- svg {
- width: 2rem;
- height: 2rem;
- margin-right: 0.2rem;
- cursor: pointer;
- }
-
- &::after {
- content: '${props => {
- if (props.likedCount > 0) return props.likedCount;
- else return '';
- }}';
- }
-`;
-
-const ArticleCommentDiv = styled.div`
- display: flex;
- flex-direction: row;
- width: 100%;
- align-items: center;
- justify-content: flex-start;
- color: ${GlobalStyled.theme.textBlue};
- font-size: 0.85rem;
-
- margin-top: 0.5rem;
- padding: 0.3rem 0.8rem 0.5rem 0.8rem;
- border-top: 1.5px solid ${GlobalStyled.theme.lineGray2};
-
- svg {
- width: 1.3rem;
- height: 1.3rem;
- margin-top: 0.2rem;
- margin-right: 0.2rem;
- }
-
- &::after {
- content: '${props => {
- if (props.commentCount > 0) return props.commentCount;
- else return '';
- }}';
- }
-`;
-
-const ArticlePageDiv = styled.div`
- display: flex;
- flex-direction: column;
- align-items: center;
-
- .comment_list_div {
- display: flex;
- flex-direction: column;
- align-items: center;
- width: 100%;
- }
- }
-
- .content_div {
- display: flex;
- flex-direction: column;
- width: 100%;
-
- .content_top {
- display: flex;
- flex-direction: row;
- align-items: flex-end;
- padding: 0.7rem;
- width: 100%;
- border-bottom: 1px solid ${GlobalStyled.theme.lineGray2};
-
- .title {
- display: flex;
- flex-direction: column;
- width: 100%;
-
- h1 {
- font-size: 1rem;
- font-weight: 600;
- color: ${GlobalStyled.theme.textBlack};
- margin-bottom: 0.2rem;
- padding-right: 0.5rem;
- }
-
- .info {
- display: flex;
- flex-direction: row;
- align-items: center;
-
- h2 {
- color: ${GlobalStyled.theme.textGray3};
- font-size: 0.7rem;
- font-weight: 300;
- margin-right: 0.8rem;
- }
-
- .edit_article {
- display: flex;
- flex-direction: row;
- align-items: flex-end;
- margin: 0 0.5rem;
- margin-left: auto;
-
- button {
- padding: 0;
- border: none;
- width: max-content;
- background: none;
- cursor: pointer;
- margin: 0 0.3rem;
- font-size: 0.7rem;
- font-weight: 400;
- }
- }
- }
- }
- }
-
- .toastui-editor-contents {
- padding: 0.7rem;
- }
- }
-`;
-
-const CommentDiv = styled.div`
- border-top: 1px solid #e6e6e6;
- padding: 0.5rem 0.8rem;
- width: 100%;
- display: flex;
- flex-direction: column;
-
- .info {
- margin-bottom: 0.4rem;
- display: flex;
- flex-direction: row;
- align-items: center;
- ${props => props.isMine && `flex-direction: row-reverse;`}
-
- .text {
- margin: 0rem 0.7rem;
- ${props => props.isMine && `text-align: right;`}
- h1 {
- color: ${GlobalStyled.theme.textBlack};
- font-size: 0.9rem;
- font-weight: 600;
- line-height: 1.1;
- }
- h2 {
- color: ${GlobalStyled.theme.textGray3};
- font-size: 0.4rem;
- font-weight: 400;
- }
- }
- }
-
- .text {
- margin-left: 0.2rem;
- word-break: break-all;
- }
+const ProfileImage = styled.img`
+ ${props => (props.width ? `width: ${props.width};` : 'width: 2.5rem;')}
+ ${props =>
+ props.width ? `min-width: ${props.width};` : 'min-width: 2.5rem;'}
+ ${props => (props.width ? `height: ${props.width};` : 'height: 2.5rem;')}
+ border-radius: 10%;
+ border: 0px;
`;
const Styled = {
ArticlePageDiv,
- ProfileImage,
+ ArticleViewDiv,
+ ArticleReactionDiv,
+ CommentsCountDiv,
+ CommentViewDiv,
CommentContent,
- ArticleLikedDiv,
- ArticleCommentDiv,
- CreateCommentDiv,
- CommentDiv,
+ CreateCommentViewDiv,
+ ProfileImage,
};
export default Styled;
diff --git a/src/Pages/ArticlePage/Components/Article.jsx b/src/Pages/ArticlePage/Components/Article.jsx
new file mode 100644
index 0000000..fa278de
--- /dev/null
+++ b/src/Pages/ArticlePage/Components/Article.jsx
@@ -0,0 +1,44 @@
+import { useNavigate } from 'react-router-dom';
+
+import '@toast-ui/editor/dist/toastui-editor.css';
+
+import { getCategoryById, getProfile } from 'Utils';
+import { useArticle, useLikeArticle, useDeleteArticle } from './hooks';
+import { getArticleTime } from 'Utils/dayjsUtils';
+import ArticleView from './ArticleView';
+
+const Article = ({ articleId, currentUserId }) => {
+ const navi = useNavigate();
+ const { data } = useArticle(articleId);
+ const likeArticle = useLikeArticle(articleId);
+ const deleteArticle = useDeleteArticle(articleId);
+
+ const handleClickEdit = () => {
+ navi(`/article/${articleId}/edit`, { state: { article: data } });
+ };
+
+ const handleClickCategory = () => {
+ navi(`/category/${data.categoryId}`);
+ };
+
+ const props = {
+ handleClickCategory,
+ category: getCategoryById(data.categoryId),
+ title: data.title,
+ writer: data.writer.nickname,
+ time: getArticleTime(data.createdAt),
+ viewCount: data.viewCount,
+ isModifiable: data.writer.id === currentUserId,
+ handleClickEdit,
+ handleClickDelete: deleteArticle.mutate,
+ profileSrc: getProfile.findProfileById(data.writer.character),
+ content: data.content,
+ isReactionPossible: data.categoryId !== 3,
+ likeCount: data.likeCount,
+ handleClickLike: likeArticle.mutate,
+ isLike: data.isLike,
+ };
+ return ;
+};
+
+export default Article;
diff --git a/src/Pages/ArticlePage/Components/ArticleView.jsx b/src/Pages/ArticlePage/Components/ArticleView.jsx
new file mode 100644
index 0000000..7b0bfae
--- /dev/null
+++ b/src/Pages/ArticlePage/Components/ArticleView.jsx
@@ -0,0 +1,63 @@
+import { Viewer } from '@toast-ui/react-editor';
+
+import { FavoriteBorder } from '@mui/icons-material';
+import FavoriteIcon from '@mui/icons-material/Favorite';
+
+import GlobalStyled from 'Styled/Global.styled';
+import Styled from '../ArticlePage.styled';
+
+const ArticleView = ({
+ handleClickCategory,
+ category,
+ title,
+ writer,
+ time,
+ viewCount,
+ isModifiable,
+ handleClickEdit,
+ handleClickDelete,
+ profileSrc,
+ content,
+ isReactionPossible,
+ likeCount,
+ handleClickLike,
+ isLike,
+}) => {
+ return (
+
+
+ {category}
+
+
+
+
{title}
+
+
{writer}
+
{time}
+
조회수 {viewCount}
+ {isModifiable && (
+
+
+
+
+ )}
+
+
+
+
+
+ {isReactionPossible && (
+
+
+ {isLike ? : }
+
+
+ )}
+
+ );
+};
+
+export default ArticleView;
diff --git a/src/Pages/ArticlePage/Components/Body.jsx b/src/Pages/ArticlePage/Components/Body.jsx
deleted file mode 100644
index c0df907..0000000
--- a/src/Pages/ArticlePage/Components/Body.jsx
+++ /dev/null
@@ -1,107 +0,0 @@
-import { useState, useEffect, useContext } from 'react';
-import { useNavigate } from 'react-router-dom';
-
-import '@toast-ui/editor/dist/toastui-editor.css';
-import { Viewer } from '@toast-ui/react-editor';
-
-import { AuthContext } from 'App';
-import { getCategoryById, getProfile } from 'Utils';
-import { ArticleService, ReactionService } from 'Network';
-import dayjs from 'dayjs';
-
-import { CommentContainer } from '.';
-import { FavoriteBorder } from '@mui/icons-material';
-import FavoriteIcon from '@mui/icons-material/Favorite';
-
-import GlobalStyled from 'Styled/Global.styled';
-import Styled from '../ArticlePage.styled';
-
-const Body = ({ articleId, categoryId }) => {
- const [article, setArticle] = useState();
- const [isLike, setIsLike] = useState(false);
- const [likeCount, setLikeCount] = useState(0);
- const navi = useNavigate();
- const handleClickEdit = () => {
- navi(`/article/${articleId}/edit`, { state: { article } });
- };
- const handleClickDelete = async () => {
- await ArticleService.deleteArticles(articleId);
- navi(-1);
- };
- const { curUser } = useContext(AuthContext);
-
- useEffect(() => {
- const fetch = async () => {
- const res = await ArticleService.getArticlesById(articleId);
- setArticle(res);
- setIsLike(res.isLike);
- setLikeCount(res.likeCount);
- };
-
- fetch();
- }, []);
-
- const getArticleTime = time =>
- dayjs(time).isSame(dayjs(), 'day')
- ? dayjs(time).format('HH:mm')
- : dayjs(time).format('MM/DD');
-
- const handleClickLike = async () => {
- const data = await ReactionService.createArticleReactionHeart(articleId);
- setIsLike(data.isLike);
- setLikeCount(data.likeCount);
- };
-
- if (!article) return <>>;
- return (
- <>
-
-
{
- navi(`/category/${article.categoryId}`);
- }}
- >
-
- {getCategoryById(article.categoryId)}
-
-
-
-
-
{article.title}
-
-
{article.writer.nickname}
-
{getArticleTime(article.createdAt)}
-
조회수 {article.viewCount}
- {article.writer.id === curUser.id && (
-
-
-
-
- )}
-
-
-
-
- {/*
{article.content}
*/}
-
-
- {categoryId !== 3 && (
-
-
- {isLike ? : }
-
-
- )}
-
-
- {article && article.categoryId !== 3 && (
-
- )}
- >
- );
-};
-
-export default Body;
diff --git a/src/Pages/ArticlePage/Components/Comment.jsx b/src/Pages/ArticlePage/Components/Comment.jsx
index d7aad4c..d6a1e79 100644
--- a/src/Pages/ArticlePage/Components/Comment.jsx
+++ b/src/Pages/ArticlePage/Components/Comment.jsx
@@ -1,76 +1,27 @@
-import { useState } from 'react';
+import React from 'react';
import { getProfile } from 'Utils';
-import { ReactionService, CommentService } from 'Network';
-import dayjs from 'dayjs';
-
-import { FavoriteBorder } from '@mui/icons-material';
-import FavoriteIcon from '@mui/icons-material/Favorite';
-
-import Styled from '../ArticlePage.styled';
-const Comment = ({
- curUser,
- articleId,
- comment,
- isLikeInitial,
- likeCountInitial,
- onDeleteComment,
-}) => {
- const [isLike, setIsLike] = useState(isLikeInitial);
- const [likeCount, setLikeCount] = useState(likeCountInitial);
-
- const getArticleTime = time =>
- dayjs(time).isSame(dayjs(), 'day')
- ? dayjs(time).format('HH:mm')
- : dayjs(time).format('MM/DD');
-
- const handleClickLike = async id => {
- const res = await ReactionService.createCommentReactionHeart(articleId, id);
-
- setIsLike(res.isLike);
- setLikeCount(res.likeCount);
- };
-
- const handleClickDelete = async commentId => {
- await CommentService.deleteComments(commentId);
- onDeleteComment(commentId);
+import { getArticleTime } from 'Utils/dayjsUtils';
+import CommentView from './CommentView';
+
+import { useDeleteComment, useLikeComment } from './hooks';
+
+const Comment = ({ currentUserId, articleId, comment }) => {
+ const likeComment = useLikeComment(comment.id, articleId);
+ const deleteComment = useDeleteComment(comment.id, articleId);
+
+ const props = {
+ isMine: currentUserId === comment.writer.id,
+ profileSrc: getProfile.findProfileById(comment.writer.character),
+ writer: comment.writer.nickname,
+ time: getArticleTime(comment.createdAt),
+ likeCount: comment.likeCount,
+ content: comment.content,
+ handleClickDelete: deleteComment.mutate,
+ handleClickLike: likeComment.mutate,
+ isLike: comment.isLike,
};
- return (
-
-
-
-
-
-
{comment?.writer?.nickname}
- {getArticleTime(comment?.updatedAt)}
-
-
-
- {comment.content}
- {curUser.id === comment.writer.id ? (
-
- ) : (
- handleClickLike(comment?.id)}
- >
- {isLike ? : }
-
- )}
-
-
- );
+ return ;
};
-export default Comment;
+export default React.memo(Comment);
diff --git a/src/Pages/ArticlePage/Components/CommentContainer.jsx b/src/Pages/ArticlePage/Components/CommentContainer.jsx
deleted file mode 100644
index f37237f..0000000
--- a/src/Pages/ArticlePage/Components/CommentContainer.jsx
+++ /dev/null
@@ -1,111 +0,0 @@
-import { useContext, useEffect, useRef, useState } from 'react';
-
-import { AuthContext } from 'App';
-import { ArticleService, CommentService } from 'Network';
-
-import Comment from './Comment';
-import Fab from '@mui/material/Fab';
-import ArrowUpwardRoundedIcon from '@mui/icons-material/ArrowUpwardRounded';
-import { SmsOutlined } from '@mui/icons-material';
-
-import Styled from '../ArticlePage.styled';
-
-const CommentContainer = ({ articleId }) => {
- const lastComment = useRef();
- const [comments, setComments] = useState([]);
- const [totalCount, setTotalCount] = useState();
- const auth = useContext(AuthContext);
-
- const handleCreateComment = newComment => {
- setComments(prev => prev.concat(newComment));
- lastComment.current.scrollIntoView();
- fetchComment();
- };
-
- const handleDeleteComment = commentId => {
- setComments(prev => prev.filter(comment => comment.id !== commentId));
- fetchComment();
- };
- const fetchComment = async () => {
- const res = await ArticleService.getArticlesCommentsById(
- articleId,
- 'ASC',
- 1,
- 1000, // 한번에 1000개 긁어옴. 어떻게 할 지 결정 후 바꿔야 함.
- );
- setComments(res?.data || []);
- setTotalCount(res?.meta.totalCount);
- };
-
- useEffect(() => {
- fetchComment();
- }, []);
-
- return (
-
-
-
-
- {comments &&
- comments.map(comment => (
-
- ))}
-
-
-
-
-
- );
-};
-
-const CreateComment = ({ articleId, handleCreateComment }) => {
- const [input, setInput] = useState('');
- const handleChange = e => {
- if (e.target.value.length < 420) {
- setInput(e.target.value);
- }
- };
- const handleClickSubmit = async e => {
- e.preventDefault();
- if (input === '') {
- return;
- }
-
- const res = await CommentService.createComments({
- content: input,
- articleId: +articleId,
- });
- if (res) {
- handleCreateComment(res);
- setInput('');
- }
- };
- return (
-
- );
-};
-
-export default CommentContainer;
diff --git a/src/Pages/ArticlePage/Components/CommentView.jsx b/src/Pages/ArticlePage/Components/CommentView.jsx
new file mode 100644
index 0000000..69ffa15
--- /dev/null
+++ b/src/Pages/ArticlePage/Components/CommentView.jsx
@@ -0,0 +1,45 @@
+import React from 'react';
+
+import { FavoriteBorder } from '@mui/icons-material';
+import FavoriteIcon from '@mui/icons-material/Favorite';
+
+import Styled from '../ArticlePage.styled';
+
+const CommentView = ({
+ isMine,
+ profileSrc,
+ writer,
+ time,
+ likeCount,
+ content,
+ handleClickDelete,
+ handleClickLike,
+ isLike,
+}) => {
+ return (
+
+
+
+
+
+
{writer}
+ {time}
+
+
+
+ {content}
+ {isMine ? (
+
+ ) : (
+
+ {isLike ? : }
+
+ )}
+
+
+ );
+};
+
+export default React.memo(CommentView);
diff --git a/src/Pages/ArticlePage/Components/Comments.jsx b/src/Pages/ArticlePage/Components/Comments.jsx
new file mode 100644
index 0000000..604d8f8
--- /dev/null
+++ b/src/Pages/ArticlePage/Components/Comments.jsx
@@ -0,0 +1,38 @@
+import React from 'react';
+
+import { useArticle, useComments } from './hooks';
+import Comment from './Comment';
+import { SmsOutlined } from '@mui/icons-material';
+
+import Styled from '../ArticlePage.styled';
+
+const Comments = ({ articleId, currentUserId }) => {
+ const {
+ data: { categoryId },
+ } = useArticle(articleId);
+ const {
+ data: { meta, data },
+ } = useComments(articleId, 'ASC', 1, 1000);
+
+ return (
+
+
+
+
+ {categoryId !== 3 &&
+ data.map(comment => (
+
+ ))}
+
+ );
+};
+
+export default React.memo(Comments);
diff --git a/src/Pages/ArticlePage/Components/CreateComment.jsx b/src/Pages/ArticlePage/Components/CreateComment.jsx
new file mode 100644
index 0000000..1390bb3
--- /dev/null
+++ b/src/Pages/ArticlePage/Components/CreateComment.jsx
@@ -0,0 +1,28 @@
+import { useRef } from 'react';
+
+import CreateCommentView from './CreateCommentView';
+import useInput from './useInput';
+import { useArticle, useComments, useCreateComment } from './hooks';
+
+const CreateComment = ({ articleId }) => {
+ const [input, handleChange, reset] = useInput();
+ const lastComment = useRef();
+ const createComment = useCreateComment(input, articleId, lastComment);
+
+ const handleClickSubmit = e => {
+ e.preventDefault();
+ createComment.mutate();
+ reset();
+ };
+
+ const props = {
+ handleClickSubmit,
+ input,
+ handleChange,
+ placeholder: '댓글을 입력하세요',
+ lastComment,
+ };
+ return ;
+};
+
+export default CreateComment;
diff --git a/src/Pages/ArticlePage/Components/CreateCommentView.jsx b/src/Pages/ArticlePage/Components/CreateCommentView.jsx
new file mode 100644
index 0000000..1e7f2a5
--- /dev/null
+++ b/src/Pages/ArticlePage/Components/CreateCommentView.jsx
@@ -0,0 +1,33 @@
+import Fab from '@mui/material/Fab';
+import ArrowUpwardRoundedIcon from '@mui/icons-material/ArrowUpwardRounded';
+
+import Styled from '../ArticlePage.styled';
+
+const CreateCommentView = ({
+ handleClickSubmit,
+ input,
+ handleChange,
+ placeholder,
+ lastComment,
+}) => {
+ return (
+ <>
+
+
+
+
+ >
+ );
+};
+
+export default CreateCommentView;
diff --git a/src/Pages/ArticlePage/Components/hooks.js b/src/Pages/ArticlePage/Components/hooks.js
new file mode 100644
index 0000000..a7b96f9
--- /dev/null
+++ b/src/Pages/ArticlePage/Components/hooks.js
@@ -0,0 +1,128 @@
+import { ArticleService, ReactionService, CommentService } from 'Network';
+import { useQuery, useMutation, useQueryClient } from 'react-query';
+import { useNavigate } from 'react-router';
+
+export function useArticle(articleId) {
+ return useQuery(
+ ['getArticleById', articleId],
+ async () => await ArticleService.getArticleById(articleId),
+ { suspense: true },
+ );
+}
+
+export function useComments(articleId, order, page, take) {
+ return useQuery(['getCommentsByArticleId', articleId], async () => {
+ return await ArticleService.getArticlesCommentsById(
+ articleId,
+ order,
+ page,
+ take,
+ );
+ });
+}
+
+export function useLikeArticle(articleId) {
+ const queryClient = useQueryClient();
+ return useMutation(
+ async () => await ReactionService.createArticleReactionHeart(articleId),
+ {
+ onSuccess: result => {
+ queryClient.setQueryData(['getArticleById', articleId], data => {
+ return {
+ ...data,
+ isLike: result.isLike,
+ likeCount: result.likeCount,
+ };
+ });
+ },
+ },
+ );
+}
+
+export function useDeleteArticle(articleId) {
+ const navigate = useNavigate();
+ return useMutation(
+ async () => await ArticleService.deleteArticles(articleId),
+ {
+ onSuccess: () => {
+ navigate(-1);
+ },
+ },
+ );
+}
+
+// 좀 보기 싫긴 함..
+export function useCreateComment(input, articleId, lastComment) {
+ const queryClient = useQueryClient();
+ return useMutation(
+ async () => {
+ return await CommentService.createComments({
+ content: input,
+ articleId: +articleId,
+ });
+ },
+ {
+ onSuccess: async () => {
+ await queryClient.invalidateQueries([
+ 'getCommentsByArticleId',
+ articleId,
+ ]);
+ if (lastComment.current) lastComment.current.scrollIntoView();
+ },
+ },
+ );
+}
+
+export function useLikeComment(commentId, articleId) {
+ const queryClient = useQueryClient();
+ return useMutation(
+ async () =>
+ await ReactionService.createCommentReactionHeart(articleId, commentId),
+ {
+ // onMutate: () => {
+ // return { id: commentId };
+ // },
+ // onSuccess: (result, _, { id }) => {
+ // queryClient.setQueryData(
+ // ['getCommentsByArticleId', articleId],
+ // data => {
+ // console.log(data.data.find(comment => comment.id === id)['isLike']);
+ // return { ...data };
+ // },
+ // );
+ // },
+ onSuccess: () => {
+ // 새로 fetch
+ queryClient.invalidateQueries(['getCommentsByArticleId', articleId]);
+ },
+ },
+ );
+}
+
+export function useDeleteComment(commentId, articleId) {
+ const queryClient = useQueryClient();
+ return useMutation(
+ async () => await CommentService.deleteComments(commentId),
+ {
+ onMutate: () => {
+ return { id: commentId };
+ },
+ onSuccess: (_, __, { id }) => {
+ // 새로 fetch
+ queryClient.invalidateQueries(['getCommentsByArticleId', articleId]);
+
+ // 얘는 캐시된 데이터만 프론트 자체적으로 업데이트
+ // meta 정보가 새로 업데이트 돼야 해서 아마 새로 fetch해와야할듯..?
+ // queryClient.setQueryData(
+ // ['getCommentsByArticleId', articleId],
+ // data => {
+ // const newData = data.data.filter(comment => comment.id !== id);
+ // console.log(data.meta);
+ // data.meta.totalCount -= 1;
+ // return { data: newData, meta: data.meta };
+ // },
+ // );
+ },
+ },
+ );
+}
diff --git a/src/Pages/ArticlePage/Components/index.jsx b/src/Pages/ArticlePage/Components/index.jsx
index 2acb63a..2d4ac59 100644
--- a/src/Pages/ArticlePage/Components/index.jsx
+++ b/src/Pages/ArticlePage/Components/index.jsx
@@ -1,4 +1,4 @@
-import Body from './Body';
-import CommentContainer from './CommentContainer';
+import Article from './Article';
+import Comments from './Comments';
-export { Body, CommentContainer };
+export { Article, Comments };
diff --git a/src/Pages/ArticlePage/Components/useInput.js b/src/Pages/ArticlePage/Components/useInput.js
new file mode 100644
index 0000000..c1ae89f
--- /dev/null
+++ b/src/Pages/ArticlePage/Components/useInput.js
@@ -0,0 +1,15 @@
+import { useCallback, useState } from 'react';
+
+function useInput() {
+ const [input, setInput] = useState('');
+
+ const handleChange = useCallback(e => {
+ if (e.target.value.length < 420) setInput(e.target.value);
+ }, []);
+
+ const reset = useCallback(() => setInput(''), []);
+
+ return [input, handleChange, reset];
+}
+
+export default useInput;
diff --git a/src/Pages/CategoryPage/Components/CategoryBody.jsx b/src/Pages/CategoryPage/Components/CategoryBody.jsx
index 10a24cd..6c486ac 100644
--- a/src/Pages/CategoryPage/Components/CategoryBody.jsx
+++ b/src/Pages/CategoryPage/Components/CategoryBody.jsx
@@ -22,7 +22,6 @@ const CategoryBody = () => {
// const [hasNextPage, setHasNextPage] = useState(true);
let hasNextPage = true;
const [target, setTarget] = useState(null);
- const [curCate, setCurCate] = useState('');
const cateList = ['자유 게시판', '익명 게시판', '공지 게시판'];
const loca = useLocation();
const navi = useNavigate();
@@ -36,37 +35,13 @@ const CategoryBody = () => {
navi(`/article/${id}`);
};
- // const setInitalArticles = async () => {
- // setIsLoaded(true);
- // const result = await ArticleService.getArticles(categoryId);
- // console.log('result ,', result);
- // const meta = result.meta;
- // setArticles(result.data);
- // setIsLoaded(false);
- // setHasNextPage(meta.hasNextPage);
- // hasNextPage = meta.hasNextPage;
- // };
-
const handleChangeCate = id => {
navi(`/category/${parseInt(id) + 1}`);
};
-
- useEffect(() => {
- if (categoryId > 3) {
- alert('준비 중입니다!');
- navi('/');
- }
- setCurCate(getCategoryByUrl(loca));
- }, [categoryId]);
- // 동기적으로 sleep하는 함수
- // const sleep = delay => {
- // let start = new Date().getTime();
- // while (new Date().getTime() < start + delay);
- // };
-
- const getMoreItem = async () => {
+ const getMoreArticles = async () => {
if (!hasNextPage) return;
+
setIsLoaded(true);
const result = await ArticleService.getArticles(categoryId, page);
const newData = result.data;
@@ -81,19 +56,23 @@ const CategoryBody = () => {
const onIntersect = async ([entry], observer) => {
if (entry.isIntersecting && !isLoaded) {
+ // if (page === 1 && hasNextPage) console.log("리렌더링 확인")
observer.unobserve(entry.target);
- await getMoreItem();
+ await getMoreArticles();
observer.observe(entry.target);
}
};
+ // 존재하지 않는 categortId 일 경우의 예외 처리, 하드 코딩.
useEffect(() => {
- if (categoryId > 3) {
- alert('준비 중입니다!');
- navi('/');
+ if (categoryId > 3 || categoryId == 2) {
+ navi('/error');
}
- setCurCate(getCategoryByUrl(loca));
- }, []);
+ setArticles([]);
+ // 리렌더링 되면서 let으로 선언한 page, hasNextPage가 자동으로 초기화 되므로 state만 초기화 주면 된다.
+ // page = 1;
+ // hasNextPage = true;
+ }, [categoryId]);
useEffect(() => {
let observer;
@@ -105,7 +84,7 @@ const CategoryBody = () => {
observer.observe(target); // observer가 해당 객체를 감시하여 변경된다면 onIntersect 콜백 함수를 실행할 것이다.
}
return () => observer && observer.disconnect(); // 주석 씌워도 잘 돌아가네?
- }, [target]);
+ }, [target, categoryId]);
return (
<>
@@ -119,6 +98,7 @@ const CategoryBody = () => {
}}
>
{cateList.map((cate, idx) => {
+ if (idx === 1) return;
return ;
})}
diff --git a/src/Pages/CreateArticlePage/Components/CreateArticleBody.jsx b/src/Pages/CreateArticlePage/Components/CreateArticleBody.jsx
index 9691728..8b9c04d 100644
--- a/src/Pages/CreateArticlePage/Components/CreateArticleBody.jsx
+++ b/src/Pages/CreateArticlePage/Components/CreateArticleBody.jsx
@@ -18,7 +18,7 @@ const CreateArticleBody = () => {
const [title, setTitle] = useState('');
const [content, setContent] = useState('');
const [curCate, setCurCate] = useState(0);
- const cateList = ['자유 게시판', '익명 게시판'];
+ const cateList = ['자유 게시판'];
const [isSending, setIsSending] = useState(false);
const [anchorEl, setAnchorEl] = useState(null);
diff --git a/src/Pages/MainPage/Components/Community.jsx b/src/Pages/MainPage/Components/Community.jsx
index 6120a7c..560ace0 100644
--- a/src/Pages/MainPage/Components/Community.jsx
+++ b/src/Pages/MainPage/Components/Community.jsx
@@ -14,11 +14,7 @@ const Community = ({
}) => {
return (
<>
-
+
{bestArticles &&
bestArticles.map(article => {
return (
@@ -61,8 +57,8 @@ const Community = ({
);
})}
- {/* margin="0.4rem" */}
-
+ {/*
);
})}
-
+ */}
>
);
};
diff --git a/src/Pages/MainPage/Components/MainBody.jsx b/src/Pages/MainPage/Components/MainBody.jsx
index 6b5fad2..296cdc2 100644
--- a/src/Pages/MainPage/Components/MainBody.jsx
+++ b/src/Pages/MainPage/Components/MainBody.jsx
@@ -41,17 +41,17 @@ const MainBody = () => {
useEffect(() => {
const getFreeArticles = async () => {
- const response = await ArticleService.getArticles(1);
+ const response = await ArticleService.getAllArticles(1);
setFreeArticles(response.data);
};
const getAnonyArticles = async () => {
- const response = await ArticleService.getArticles(2);
+ const response = await ArticleService.getAllArticles(2);
setAnonyArticles(response.data);
};
const getNotiArticles = async () => {
- const response = await ArticleService.getArticles(3);
+ const response = await ArticleService.getAllArticles(3);
setNotiArticles(response.data);
};
@@ -61,7 +61,7 @@ const MainBody = () => {
};
getBestArticles();
getFreeArticles();
- getAnonyArticles();
+ // getAnonyArticles();
getNotiArticles();
}, []);
diff --git a/src/Styled/Global.styled.js b/src/Styled/Global.styled.js
index c551e69..bd309b4 100644
--- a/src/Styled/Global.styled.js
+++ b/src/Styled/Global.styled.js
@@ -55,7 +55,7 @@ const theme = {
};
const assets = {
- headerLogo: '/assets/42mainlogo2.svg',
+ headerLogo: '/assets/42mainlogo3.png',
sidebar: {
'80000co': '/assets/sidebar/80000co.png',
humansof42: '/assets/sidebar/humansof42.png',
diff --git a/src/Utils/dayjsUtils.js b/src/Utils/dayjsUtils.js
new file mode 100644
index 0000000..454a52a
--- /dev/null
+++ b/src/Utils/dayjsUtils.js
@@ -0,0 +1,13 @@
+import dayjs from 'dayjs';
+
+export const getArticleTime = time =>
+ dayjs(time).isSame(dayjs(), 'day')
+ ? dayjs(time).format('HH:mm')
+ : dayjs(time).format('MM/DD');
+
+export const isNewArticle = time =>
+ dayjs().isBefore(dayjs(time).add(12, 'hour'));
+
+const dayjsUtils = { getArticleTime, isNewArticle };
+
+export default dayjsUtils;