Skip to content

Commit

Permalink
Feature : 커뮤니티 댓글 기능 추가 (#595)
Browse files Browse the repository at this point in the history
* feat: Community 게시글 댓글 조회 API 연동

* feat: 댓글 작성 API 연동

* feat: 댓글 좋아요 API 연동

* feat: 댓글 삭제 API 연동

* feat: CardHeader가 children을 선택적으로 받을 수 있도록 수정

* feat: 대댓글(Reply) 생성 API 연결

* feat: 대댓글(Reply) 조회 API 연결

* feat: 생성 날짜 포멧 변경

* feat: Avatar에서 국기 이미지를 보여주도록 수정

* feat: 글자 튀어나감 방지

* feat: CommentForm i18n 적용

* refactor: CommentProvider 이름 변경

* refactor: ArticleDetail commentsList -> commentList 수정

* refactor: 경로 수정, button -> submit, form -> onSubmit 수정

* refactor: ArticleDetail CommentSection -> CommentProvider

* refactor: CommentList pb-102 -> Spacing 컴포넌트로 수정

* refactor: className + 제거 cn 으로 수정

* refactor: children && 연산 제거

* refactor: CommentForm input -> textarea 수정

* refactor: CardHeader children -> PropsWithChildren수정

---------

Co-authored-by: Kuesung Park <gueit214@naver.com>
  • Loading branch information
dev-dong-su and guesung authored Jan 26, 2024
1 parent 59b32c2 commit f911776
Show file tree
Hide file tree
Showing 21 changed files with 648 additions and 93 deletions.
38 changes: 38 additions & 0 deletions src/apis/community/apis.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
import {
CreateArticleRequest,
CreateArticleResponse,
CreateCommentRequest,
CreateReplyRequest,
GetArticleDetail,
GetArticlesRequest,
GetArticlesResponse,
GetCommunityCommentsResponse,
GetReplyResponse,
} from '@/apis/community/type';
import privateApi from '@/apis/config/privateApi';

Expand Down Expand Up @@ -33,3 +37,37 @@ export const postCommunityArticleLike = (articleId: number) => {
export const postDeleteCommunityArticle = (articleId: number) => {
return privateApi.post(`/communities/articles/${articleId}/delete`);
};

export const getCommunityComments = (articleId: number) => {
return privateApi.get<GetCommunityCommentsResponse>(
`/communities/articles/${articleId}/comments`
);
};

export const postCreateCommunityComment = ({
params: { articleId },
payload,
}: CreateCommentRequest) => {
return privateApi.post(`/communities/articles/${articleId}/comments`, payload);
};

export const postCommunityCommentLike = (articleId: number, commentId: number) => {
return privateApi.post(`/communities/articles/${articleId}/comments/${commentId}/like`);
};

export const deleteCommunityCommentLike = (articleId: number, commentId: number) => {
return privateApi.delete(`/communities/articles/${articleId}/comments/${commentId}`);
};

export const getCommunityReply = (articleId: number, commentId: number) => {
return privateApi.get<GetReplyResponse>(
`/communities/articles/${articleId}/comments/${commentId}/child`
);
};

export const postCreateCommunityReply = ({
params: { articleId, commentId },
payload,
}: CreateReplyRequest) => {
return privateApi.post(`/communities/articles/${articleId}/comments/${commentId}/child`, payload);
};
6 changes: 6 additions & 0 deletions src/apis/community/keys.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
export const Keys = Object.freeze({
getCommunityArticles: (categoryId?: number) => ['getCommunityArticles', categoryId],
getCommunityArticleDetail: (articleId: number) => ['getCommunityArticleDetail', articleId],
getCommunityComments: (articleId: number) => ['getCommunityComments', articleId],
getCommunityReply: (articleId: number, commentId: number) => [
'getCommunityReply',
articleId,
commentId,
],
});
77 changes: 75 additions & 2 deletions src/apis/community/mutations.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,17 @@
import { useMutation, useQueryClient } from '@tanstack/react-query';

import {
deleteCommunityCommentLike,
postCommunityArticleLike,
postCommunityCommentLike,
postCreateCommunityArticle,
postCreateCommunityComment,
postCreateCommunityReply,
postDeleteCommunityArticle,
} from '@/apis/community/apis';
import { Keys } from '@/apis/community/keys';
import { CommunityArticle } from '@/apis/community/type';
import useAppRouter from '@/hooks/useAppRouter';
import { useMutation, useQueryClient } from '@tanstack/react-query';

export const usePostCreateCommunityArticle = () => {
const queryClient = useQueryClient();
Expand All @@ -16,7 +21,6 @@ export const usePostCreateCommunityArticle = () => {
mutationFn: postCreateCommunityArticle,
onSuccess: (data, variables) => {
const { categoryId } = variables;
console.log(categoryId);
queryClient.invalidateQueries({ queryKey: Keys.getCommunityArticles(0) }); // 전체 카테고리
queryClient.invalidateQueries({ queryKey: Keys.getCommunityArticles(categoryId) }); // 작성한 게시글 카테고리
back();
Expand Down Expand Up @@ -75,3 +79,72 @@ export const usePostDeleteCommunityArticle = (articleId: number, categoryId: num
},
});
};

export const usePostCreateComment = (articleId: number) => {
const queryClient = useQueryClient();

return useMutation({
mutationFn: postCreateCommunityComment,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: Keys.getCommunityArticleDetail(articleId) });
queryClient.invalidateQueries({ queryKey: Keys.getCommunityComments(articleId) });
},
});
};

export const usePostCommunityCommentLike = (articleId: number, commentId: number) => {
const queryClient = useQueryClient();

return useMutation({
mutationFn: () => postCommunityCommentLike(articleId, commentId),
onMutate: async () => {
// 이전 상태를 저장
const previousArticle = queryClient.getQueryData(Keys.getCommunityComments(articleId));

// 낙관적 업데이트를 위해 쿼리 데이터를 즉시 변경
queryClient.setQueryData(
Keys.getCommunityComments(articleId),
(oldData: CommunityArticle) => {
return { ...oldData, article: { ...oldData.article, isLiked: true } };
}
);

return { previousArticle };
},
// 에러가 발생하면 이전 상태로 되돌림
onError: (error, variables, context) => {
if (context?.previousArticle) {
queryClient.setQueryData(Keys.getCommunityComments(articleId), context.previousArticle);
} else throw new Error('No previous Data');
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: Keys.getCommunityComments(articleId) });
},
});
};

export const useDeleteCommunityComment = (articleId: number, commentId: number) => {
const queryClient = useQueryClient();

return useMutation({
mutationFn: () => deleteCommunityCommentLike(articleId, commentId),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: Keys.getCommunityComments(articleId) });
},
});
};

export const useCreateCommunityReply = (articleId: number) => {
const queryClient = useQueryClient();

return useMutation({
mutationFn: postCreateCommunityReply,
onSuccess: (data, variables) => {
const { params } = variables;
queryClient.invalidateQueries({
queryKey: Keys.getCommunityReply(articleId, params.commentId),
});
queryClient.invalidateQueries({ queryKey: Keys.getCommunityComments(articleId) });
},
});
};
21 changes: 20 additions & 1 deletion src/apis/community/queries.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
import { useSuspenseInfiniteQuery, useSuspenseQuery } from '@tanstack/react-query';

import { getCommunityArticleDetail, getCommunityArticles } from '@/apis/community/apis';
import {
getCommunityArticleDetail,
getCommunityArticles,
getCommunityComments,
getCommunityReply,
} from '@/apis/community/apis';
import { Keys } from '@/apis/community/keys';

export const useGetCommunityArticles = (categoryId: number) => {
Expand All @@ -26,3 +31,17 @@ export const useGetCommunityArticleDetail = (articleId: number) => {
queryFn: () => getCommunityArticleDetail(articleId),
});
};

export const useGetCommunityComments = (articleId: number) => {
return useSuspenseQuery({
queryKey: Keys.getCommunityComments(articleId),
queryFn: () => getCommunityComments(articleId),
});
};

export const useGetCommunityReply = (articleId: number, commentId: number) => {
return useSuspenseQuery({
queryKey: Keys.getCommunityReply(articleId, commentId),
queryFn: () => getCommunityReply(articleId, commentId),
});
};
82 changes: 82 additions & 0 deletions src/apis/community/type.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,4 +74,86 @@ export interface GetArticleDetail {
data: CommunityArticle;
}

export interface GetCommunityCommentsResponse {
meta: {
statusCode: number;
message: string;
};
data: {
comments: Comment[];
};
}

export interface CreateCommentRequest {
params: { articleId: number };
payload: {
content: string;
};
}

export interface Comment {
comment: {
id: number;
isWriter: boolean;
isLiked: boolean;
userId: number;
articleId: number;
content: string;
likeCount: number;
commentCount: number;
createdAt: string;
updatedAt: string;
};
writer: {
id: number;
isCertifiedStudent: boolean;
profileImage: string;
nickName: string;
countryName: string;
countryImage: string;
reliabilityLevel: ReliabilityType;
};
}

export interface GetReplyResponse {
meta: {
statusCode: number;
message: string;
};
data: {
childComments: Reply[];
};
}

export interface CreateReplyRequest {
params: { articleId: number; commentId: number };
payload: {
content: string;
};
}

export interface Reply {
childComment: {
id: number;
isWriter: boolean;
isLiked: boolean;
userId: number;
articleId: number;
content: string;
likeCount: number;
commentCount: number;
createdAt: string;
updatedAt: string;
};
writer: {
id: number;
isCertifiedStudent: boolean;
profileImage: string;
nickName: string;
countryName: string;
countryImage: string;
reliabilityLevel: ReliabilityType;
};
}

export type CategoryType = 'K-POP' | 'Q&A' | 'Language';
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
'use client';

import { useGetCommunityArticleDetail } from '@/apis/community';
import ArticleItem from '@/app/[lng]/(main)/community/[articleId]/components/ArticleItem';
import ArticleItem from './ArticleItem';
import CommentForm from './CommentForm';
import CommentList from './CommentList';
import CommentProvider from './CommentProvider';
import { useGetCommunityArticleDetail, useGetCommunityComments } from '@/apis/community';
import { useTranslation } from '@/app/i18n/client';
import { Divider } from '@/components/Divider';
import { Spacing } from '@/components/Spacing';
Expand All @@ -14,7 +17,9 @@ export default function ArticleDetail({ articleId }: ArticleDetailProps) {
const { t } = useTranslation('community');

const { data: articleData } = useGetCommunityArticleDetail(articleId);
const { data: articleComments } = useGetCommunityComments(articleId);
const commentCount = articleData.data.article.commentCount;
const commentList = articleComments.data.comments;

return (
<>
Expand All @@ -25,7 +30,12 @@ export default function ArticleDetail({ articleId }: ArticleDetailProps) {
{t('detail.commentCount', { commentCount })}
</p>
<Spacing size={8} />
{/*<CommentList commentList={DUMMY_COMMENTS_DATA} />*/}
<CommentProvider>
<CommentList commentList={commentList} articleWriterId={articleData.data.writer.id} />
<Spacing size={100} />
<CommentForm />
<Spacing size={60} />
</CommentProvider>
</>
);
}
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
import { format, parseISO } from 'date-fns';
import Image from 'next/image';

import { CommunityArticle, usePostCommunityArticleLike } from '@/apis/community';
import { useTranslation } from '@/app/i18n/client';
import { CardHeader } from '@/components/Card';
Expand All @@ -7,7 +10,6 @@ import { ImageModal } from '@/components/Modal';
import { Spacing } from '@/components/Spacing';
import { useModal } from '@/hooks/useModal';
import cn from '@/utils/cn';
import Image from 'next/image';

interface ArticleItemProps {
article: CommunityArticle;
Expand Down Expand Up @@ -57,8 +59,9 @@ export default function ArticleItem({ article }: ArticleItemProps) {
userImageUrl={profileImage}
isWriterCertifiedStudent={isCertifiedStudent}
writerReliabilityLevel={reliabilityLevel}
isWriterCaptain={isWriter}
date={createdAt}
isWriterCaptain={true}
date={format(parseISO(createdAt), 'yyyy.MM.dd HH:mm')}
countryImage={countryImage}
/>
<Spacing size={16} />
<div className={'text-2xl font-semibold'}>{title}</div>
Expand Down
Loading

0 comments on commit f911776

Please sign in to comment.