diff --git a/src/App.tsx b/src/App.tsx index d6ef6f3..1725516 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -39,7 +39,7 @@ const router = createBrowserRouter([ element: , }, { - path: '/detail/community/:id', + path: '/community/post/:id', element: , }, { diff --git a/src/assets/default_profile_image.svg b/src/assets/default_profile_image.svg new file mode 100644 index 0000000..4d01105 --- /dev/null +++ b/src/assets/default_profile_image.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/src/features/community/CommunityPage.tsx b/src/features/community/CommunityPage.tsx index c3abc54..c5dbc25 100644 --- a/src/features/community/CommunityPage.tsx +++ b/src/features/community/CommunityPage.tsx @@ -4,7 +4,7 @@ import { BeatLoader } from 'react-spinners'; import { useCallback, useEffect, useRef, useState } from 'react'; import axios from 'axios'; import { useInfiniteQuery } from '@tanstack/react-query'; -import { useSearchParams } from 'react-router-dom'; +import { useNavigate, useSearchParams } from 'react-router-dom'; import { SortKey, PostTypes } from './types'; const CommunityPage = () => { @@ -42,7 +42,7 @@ const CommunityPage = () => { }, initialPageParam: 1, }); - + const navigate = useNavigate(); const loadMoreRef = useRef(null); const handleObserver = useCallback( @@ -79,7 +79,13 @@ const CommunityPage = () => { {data?.pages.flatMap((page) => - page.map((post: PostTypes) => ) + page.map((post: PostTypes) => ( + navigate(`/community/post/${post.id}`)} + /> + )) )} {isFetchingNextPage && ( diff --git a/src/features/community/components/CommentInput.tsx b/src/features/community/components/CommentInput.tsx index 52024bc..c050b12 100644 --- a/src/features/community/components/CommentInput.tsx +++ b/src/features/community/components/CommentInput.tsx @@ -1,11 +1,49 @@ import styled from 'styled-components'; import commentUpdate from '../../../assets/comment-update.svg'; +import axios from 'axios'; +import { useState } from 'react'; + +interface CommentInputPropTypes { + postId: number | undefined; + userId: number | undefined; + refetchComments: () => void; +} + +const CommentInput = ({ + postId, + userId, + refetchComments, +}: CommentInputPropTypes) => { + const [commentData, setCommentData] = useState(''); + + const PostComment = async () => { + const trimmedComment = commentData.trim(); + + if (trimmedComment.length < 1 || trimmedComment.length > 500) { + alert('댓글은 공백이 아닌 1자 이상 500자 이하로 작성해주세요'); + return; + } + + try { + await axios.post(`/api/community/post/${postId}/comment`, { + content: commentData, + user_id: userId, + }); + setCommentData(''); + refetchComments(); + } catch (e) { + console.error('댓글 작성에 실패했습니다'); + } + }; -const CommentInput = () => { return ( - - updateBtn + setCommentData(e.target.value)} + /> + updateBtn ); }; @@ -19,6 +57,10 @@ const Wrapper = styled.div` width: 100%; height: 64px; background-color: ${({ theme }) => theme.colors.write_purple200}; + + img { + cursor: pointer; + } `; const InputLayout = styled.textarea` @@ -26,6 +68,8 @@ const InputLayout = styled.textarea` background-color: transparent; width: 100%; margin-right: 4px; + color: #ffffff; + resize: none; ::placeholder { font-size: 1rem; diff --git a/src/features/community/components/Comments.tsx b/src/features/community/components/Comments.tsx index b9245fd..60f3538 100644 --- a/src/features/community/components/Comments.tsx +++ b/src/features/community/components/Comments.tsx @@ -1,11 +1,16 @@ import styled from 'styled-components'; -import { mockPosts } from '../../../features/community/components/mockData'; -const Comments = () => { +const Comments = ({ + nickname, + content, +}: { + nickname: string; + content?: string; +}) => { return ( -

{mockPosts[0].author.nickname}

-

{mockPosts[0].comment}

+

{nickname}

+

{content}

); }; diff --git a/src/features/community/components/CommentsContent.tsx b/src/features/community/components/CommentsContent.tsx index 210c0af..76fa08e 100644 --- a/src/features/community/components/CommentsContent.tsx +++ b/src/features/community/components/CommentsContent.tsx @@ -1,13 +1,29 @@ import styled from 'styled-components'; -import { mockPosts } from '../../../features/community/components/mockData'; import { Comments, CommentInput } from './index'; +import { CommentsContentPropTypes } from '../types'; -const CommentsContent = () => { +const CommentsContent = ({ + comment, + postId, + userId, + refetchComments, +}: CommentsContentPropTypes) => { return (

댓글

- {mockPosts[0].comment && } - + {comment && + comment.map((comment) => ( + + ))} +
); }; diff --git a/src/features/community/components/DetailPostContent.tsx b/src/features/community/components/DetailPostContent.tsx index 6cfd66d..ca7c6a6 100644 --- a/src/features/community/components/DetailPostContent.tsx +++ b/src/features/community/components/DetailPostContent.tsx @@ -1,16 +1,86 @@ import styled from 'styled-components'; import { UserInfo } from './index'; +import { UserDataType } from '../types'; import { Icon } from '../../../components/ui/Icon'; -import { mockPosts } from '../../../features/community/components/mockData'; +import { useMutation } from '@tanstack/react-query'; +import { useEffect, useState } from 'react'; +import axios from 'axios'; +import { useParams } from 'react-router-dom'; +import { queryClient } from '../../../network/react-query/queryClient'; + +const DetailPostContent = ({ + content, + likesCount, + userInfo, + user_id, +}: { + content?: string; + likesCount?: number; + userInfo?: UserDataType; + user_id?: number; +}) => { + const [isLiked, setIsLiked] = useState(false); + const [currentLikes, setCurrentLikes] = useState( + likesCount === 0 ? 0 : likesCount + ); + const { id } = useParams<{ id: string }>(); + const post_id = Number(id); + + const empathyMutation = useMutation({ + mutationFn: async () => { + const response = await axios.patch( + `/api/community/post/${post_id}/like`, + {}, + { + params: { + user_id, + }, + } + ); + + const previousEmpathy = queryClient.getQueryData(['empathy']); + return { ...response.data, previousEmpathy }; + }, + onSuccess: (data) => { + if (data.is_cancled) { + setIsLiked(false); + setCurrentLikes((prev) => (prev ?? 0) - 1); + } else { + setIsLiked(true); + setCurrentLikes((prev) => (prev ?? 0) + 1); + } + }, + onError: () => { + queryClient.invalidateQueries({ queryKey: ['empathy'] }); + }, + onSettled: () => { + queryClient.invalidateQueries({ queryKey: ['empathy'] }); + }, + }); + + const handleLikeToggle = () => { + if (!isLiked) { + empathyMutation.mutate(); + } else { + setIsLiked(false); + setCurrentLikes((prev) => Math.max(0, (prev ?? 0) - 1)); + } + }; + + useEffect(() => { + setCurrentLikes(likesCount === 0 ? 0 : likesCount); + }, [likesCount]); -const DetailPostContent = () => { return ( - - {mockPosts[0].content} - - -

{mockPosts[0].likes}

+ + {content} + + +

{currentLikes}

); diff --git a/src/features/community/components/Post.tsx b/src/features/community/components/Post.tsx index 0eb6fdb..3f25c7f 100644 --- a/src/features/community/components/Post.tsx +++ b/src/features/community/components/Post.tsx @@ -1,10 +1,12 @@ import styled from 'styled-components'; import { PostContent, PostReaction, UserInfo } from './index'; -import { PostTypes } from '../types'; +import { PostProps } from '../types'; + +const Post = (postProps: PostProps) => { + const { onClick, ...props } = postProps; -const Post = (props: Partial) => { return ( - + {props.user_info && props.created_at ? ( ) : ( @@ -29,6 +31,7 @@ const Wrapper = styled.div` background-color: rgba(231, 225, 255, 0.4); margin-bottom: 36px; padding: 20px 26px; + cursor: pointer; `; export default Post; diff --git a/src/features/community/components/UserInfo.tsx b/src/features/community/components/UserInfo.tsx index 9c301a7..e53f4ad 100644 --- a/src/features/community/components/UserInfo.tsx +++ b/src/features/community/components/UserInfo.tsx @@ -1,5 +1,6 @@ import styled from 'styled-components'; import { UserDataType } from '../types'; +import default_profile_image from '../../../assets/default_profile_image.svg'; const UserInfo = ({ user_info, @@ -13,7 +14,10 @@ const UserInfo = ({ {user_info && ( <> - userImg + userImg

{user_info.nickname}

)} diff --git a/src/features/community/components/WorryContent.tsx b/src/features/community/components/WorryContent.tsx index 5e2a27d..0bd01f8 100644 --- a/src/features/community/components/WorryContent.tsx +++ b/src/features/community/components/WorryContent.tsx @@ -1,11 +1,27 @@ import styled from 'styled-components'; +import { UserDataType } from '../types'; import { DetailPostContent } from './index'; -const WorryContent = () => { +const WorryContent = ({ + userInfo, + content, + likesCount, + userId, +}: { + userInfo?: UserDataType; + content?: string; + likesCount?: number; + userId?: number; +}) => { return (

고민

- +
); }; @@ -14,6 +30,7 @@ const Wrapper = styled.div` display: flex; flex-direction: column; gap: 16px; + h2 { font-size: 20px; font-weight: 600; diff --git a/src/features/community/components/mockData.ts b/src/features/community/components/mockData.ts deleted file mode 100644 index e735562..0000000 --- a/src/features/community/components/mockData.ts +++ /dev/null @@ -1,36 +0,0 @@ -interface Author { - nickname: string; - profileUrl: string; -} - -interface Post { - id: number; - content: string; - author: Author; - date: string; - comment: string; - likes: number; - comments: number; -} - -// 목업 데이터 -const mockPosts: Post[] = [ - { - id: 1, - content: - '요즘 너무 바빠서 제 자신을 돌볼 시간이 없어요. 어떻게 균형을 잡을 수 있을까요? 요즘 너무 바빠서 제 자신을 돌볼 시간이 없어요. 어떻게 균형을 잡을 수 있을까요?요즘 너무 바빠서 제 자신을 돌볼 시간이 없어요. 어떻게 균형을 잡을 수 있을까요?요즘 너무 바빠서 제 자신을 돌볼 시간이 없어요. 어떻게 균형을 잡을 수 있을까요?요즘 너무 바빠서 제 자신을 돌볼 시간이 없어요. 어떻게 균형을 잡을 수 있을까요?요즘 너무 바빠서 제 자신을 돌볼 시간이 없어요. 어떻게 균형을 잡을 수 있을까요?요즘 너무 바빠서 제 자신을 돌볼 시간이 없어요. 어떻게 균형을 잡을 수 있을까요?요즘 너무 바빠서 제 자신을 돌볼 시간이 없어요. 어떻게 균형을 잡을 수 있을까요?', - author: { - nickname: 'user01', - profileUrl: - 'https://images2.minutemediacdn.com/image/upload/c_crop,w_5692,h_3201,x_0,y_374/c_fill,w_1350,ar_16:9,f_auto,q_auto,g_auto/images/voltaxMediaLibrary/mmsport/mentalfloss/01gq0e0b6s2972198hmw.jpg', - }, - comment: - '졸리면 잠을 자는게 사실 제일 최고인 것 같은데 너무 바쁘면,, 쪽잠이라도 자거나 영양제로 버텨보기,,아무튼 그렇기,, 글자수 테스트 중인데 몇글자지,,', - date: '2024-10-23', - likes: 15, - comments: 3, - }, -]; - -export { mockPosts }; -export type { Post }; diff --git a/src/features/community/pages/DetailCommunityPage.tsx b/src/features/community/pages/DetailCommunityPage.tsx index 6d064a3..b320e90 100644 --- a/src/features/community/pages/DetailCommunityPage.tsx +++ b/src/features/community/pages/DetailCommunityPage.tsx @@ -1,13 +1,46 @@ import styled from 'styled-components'; -import { CommentsContent, WorryContent } from '../components'; +import { CommentsContent, UserInfo, WorryContent } from '../components'; import { Title } from '../components'; +import axios from 'axios'; +import { DetailPostTypes } from '../types'; +import { useParams } from 'react-router-dom'; +import { useQuery } from '@tanstack/react-query'; const DetailCommunityPage = () => { + const { id } = useParams(); + + const getDetailPost = async (id: number): Promise => { + const { data } = await axios.get( + `/api/community/post/${id}` + ); + + return data; + }; + + const { data, refetch } = useQuery({ + queryKey: ['post', id], + queryFn: () => getDetailPost(Number(id)), + }); + console.log(data); + return ( - <WorryContent /> - <CommentsContent /> + <WorryContent + userInfo={{ + nickname: data?.nickname || '', + profile_image: data?.profile_image || '', + }} + content={data?.content} + likesCount={data?.like_count} + userId={data?.user_id} + /> + <CommentsContent + comment={data?.comments} + postId={data?.id} + userId={Number(data?.user_id)} + refetchComments={refetch} + /> </Wrapper> ); }; diff --git a/src/features/community/types/community.ts b/src/features/community/types/community.ts index 9c81b52..0f91f4c 100644 --- a/src/features/community/types/community.ts +++ b/src/features/community/types/community.ts @@ -6,9 +6,17 @@ export interface UserDataType { profile_image: string; } -export interface PostTypes { - id: string; - user_id: string; +export interface PostTypes extends BasePostTypes { + user_info: UserDataType; +} + +export interface PostContentPropTypes { + content: string; +} + +export interface BasePostTypes { + id: number; + user_id: number; emotion_type: string; content: string; ai_content: string; @@ -17,9 +25,4 @@ export interface PostTypes { is_solved: boolean; like_count: number; comment_count: number; - user_info: UserDataType; -} - -export interface PostContentPropTypes { - content: string; } diff --git a/src/features/community/types/detailCommunity.ts b/src/features/community/types/detailCommunity.ts new file mode 100644 index 0000000..17ff4a2 --- /dev/null +++ b/src/features/community/types/detailCommunity.ts @@ -0,0 +1,26 @@ +import { MouseEventHandler } from 'react'; +import { PostTypes, BasePostTypes } from './index'; + +export interface DetailPostTypes extends BasePostTypes { + comments: CommentDataType[]; + nickname: string; + profile_image: string; +} + +export interface CommentDataType { + comment_id: number; + user_id: number; + nickname: string; + content: string; + created_at: Date; +} + +export interface CommentsContentPropTypes { + comment: CommentDataType[] | undefined; + postId: number | undefined; + userId: number | undefined; + refetchComments: () => void; +} +export interface PostProps extends Partial<PostTypes> { + onClick?: MouseEventHandler<HTMLDivElement> | undefined; +} diff --git a/src/features/community/types/index.ts b/src/features/community/types/index.ts index 12bb961..9c6bd0f 100644 --- a/src/features/community/types/index.ts +++ b/src/features/community/types/index.ts @@ -1 +1,2 @@ export * from './community'; +export * from './detailCommunity';