Skip to content

Commit 1f97f2a

Browse files
authored
Merge pull request #141 from oodd-team/feat/OK-226
[OD-226] Post 리액트 쿼리 적용
2 parents 2699906 + f6bb38f commit 1f97f2a

8 files changed

Lines changed: 184 additions & 229 deletions

File tree

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212
"start:json-server": "json-server --watch db.json --port 5000"
1313
},
1414
"dependencies": {
15+
"@tanstack/react-query": "^5.70.0",
16+
"@tanstack/react-query-devtools": "^5.70.0",
1517
"@types/styled-components": "^5.1.34",
1618
"axios": "^1.7.2",
1719
"dayjs": "^1.11.12",

src/apis/post/index.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { useQuery } from '@tanstack/react-query';
2+
13
import { newRequest } from '@apis/core';
24

35
import type { EmptySuccessResponse } from '@apis/core/dto';
@@ -36,3 +38,11 @@ export const deletePostApi = (postId: number) => newRequest.delete<EmptySuccessR
3638
// 대표 게시글 지정
3739
export const modifyPostRepresentativeStatusApi = (postId: number) =>
3840
newRequest.patch<EmptySuccessResponse>(`/post/${postId}/is-representative`);
41+
42+
export const usePostDetail = (postId: number) => {
43+
return useQuery({
44+
queryKey: ['postDetail', postId],
45+
queryFn: () => getPostDetailApi(postId),
46+
enabled: !!postId, // postId가 존재할 때만 요청 수행
47+
});
48+
};

src/main.tsx

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
2+
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
13
import { createRoot } from 'react-dom/client';
24
import { RecoilRoot } from 'recoil';
35
import { ThemeProvider } from 'styled-components';
@@ -10,13 +12,18 @@ import { SocketProvider } from '@context/SocketProvider';
1012

1113
import App from './App';
1214

15+
const queryClient = new QueryClient();
16+
1317
createRoot(document.getElementById('root')!).render(
1418
<ThemeProvider theme={theme}>
15-
<GlobalStyle />
16-
<RecoilRoot>
17-
<SocketProvider>
18-
<App />
19-
</SocketProvider>
20-
</RecoilRoot>
19+
<QueryClientProvider client={queryClient}>
20+
<GlobalStyle />
21+
<RecoilRoot>
22+
<SocketProvider>
23+
<App />
24+
<ReactQueryDevtools initialIsOpen={false} />
25+
</SocketProvider>
26+
</RecoilRoot>
27+
</QueryClientProvider>
2128
</ThemeProvider>,
2229
);

src/pages/Post/PostBase/index.tsx

Lines changed: 118 additions & 126 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,14 @@
11
import { useEffect, useState, useRef } from 'react';
22
import { useNavigate, useParams } from 'react-router-dom';
33

4-
import dayjs from 'dayjs';
5-
import { useRecoilState } from 'recoil';
6-
import 'dayjs/locale/ko';
4+
import { useMutation, useQueryClient } from '@tanstack/react-query';
5+
import dayjs, { extend } from 'dayjs';
6+
import relativeTime from 'dayjs/plugin/relativeTime';
77

88
import theme from '@styles/theme';
99

10-
import { getPostDetailApi } from '@apis/post';
10+
import { usePostDetail } from '@apis/post';
1111
import { togglePostLikeStatusApi } from '@apis/post-like';
12-
import { postIdAtom, userAtom, isPostRepresentativeAtom } from '@recoil/Post/PostAtom';
1312

1413
import Left from '@assets/arrow/left.svg';
1514
import Message from '@assets/default/message.svg';
@@ -21,6 +20,7 @@ import BottomSheet from '@components/BottomSheet';
2120
import ClothingInfoItem from '@components/ClothingInfoItem';
2221
import { OODDFrame } from '@components/Frame/Frame';
2322
import NavBar from '@components/NavBar';
23+
import Skeleton from '@components/Skeleton';
2424
import { StyledText } from '@components/Text/StyledText';
2525
import TopBar from '@components/TopBar';
2626

@@ -34,28 +34,23 @@ import LikeCommentBottomSheetContent from './LikeCommentBottomSheetContent/index
3434

3535
import {
3636
PostLayout,
37-
PostContainer,
3837
PostInfoContainer,
3938
UserProfile,
4039
UserName,
4140
MenuBtn,
4241
PostContentContainer,
43-
ContentSkeleton,
4442
Content,
4543
ShowMoreButton,
46-
ImageSkeleton,
4744
IconRow,
4845
IconWrapper,
4946
Icon,
5047
ClothingInfoList,
48+
UserNameWrapper,
49+
PostWrapper,
5150
} from './styles';
5251

5352
const PostBase: React.FC<PostBaseProps> = ({ onClickMenu }) => {
54-
const [, setPostId] = useRecoilState(postIdAtom);
55-
const [post, setPost] = useState<GetPostDetailResponse['data']>();
56-
const [user, setUser] = useRecoilState(userAtom);
57-
const [, setIsPostRepresentative] = useRecoilState(isPostRepresentativeAtom);
58-
const [timeAgo, setTimeAgo] = useState<string | null>();
53+
extend(relativeTime);
5954
const [isTextOverflowing, setIsTextOverflowing] = useState(false);
6055
const [showFullText, setShowFullText] = useState(false);
6156
const [isLikeCommentBottomSheetOpen, setIsLikeCommentBottomSheetOpen] = useState(false);
@@ -64,6 +59,12 @@ const PostBase: React.FC<PostBaseProps> = ({ onClickMenu }) => {
6459
const { postId } = useParams<{ postId: string }>();
6560
const contentRef = useRef<HTMLDivElement>(null);
6661

62+
const { data, isLoading } = usePostDetail(Number(postId));
63+
const queryClient = useQueryClient();
64+
const post = data?.data;
65+
const user = post?.user;
66+
const timeAgo = dayjs(post?.createdAt).locale('ko').fromNow();
67+
6768
const nav = useNavigate();
6869

6970
const handleLikeCommentOpen = (tab: 'likes' | 'comments') => {
@@ -80,48 +81,26 @@ const PostBase: React.FC<PostBaseProps> = ({ onClickMenu }) => {
8081
};
8182

8283
// 게시글 좋아요 누르기/취소하기 api
83-
const togglePostLikeStatus = async () => {
84-
if (!post || !postId) return;
85-
86-
const prevPost = { ...post }; // 현재 상태 저장
87-
setPost({
88-
...post,
89-
isPostLike: !post.isPostLike,
90-
postLikesCount: post.isPostLike ? post.postLikesCount - 1 : post.postLikesCount + 1,
91-
}); //사용자가 좋아요를 누르면 먼저 클라이언트에서 post 상태를 변경(낙관적 업데이트)
92-
93-
try {
94-
const response = await togglePostLikeStatusApi(Number(postId));
95-
setPost({
96-
...post,
97-
isPostLike: response.data.isPostLike,
98-
postLikesCount: response.data.postLikesCount,
99-
}); // 서버로 요청 후 성공하면 그대로 유지
100-
} catch (error) {
101-
console.error('Error toggling like status:', error);
102-
setPost(prevPost); // 실패하면 원래 상태로 롤백
103-
}
104-
};
105-
106-
useEffect(() => {
107-
setPostId(Number(postId));
108-
109-
// 게시글 정보 가져오기
110-
const getPost = async () => {
111-
try {
112-
const response = await getPostDetailApi(Number(postId));
113-
const data = response.data;
114-
setPost(data);
115-
setUser(data.user);
116-
setIsPostRepresentative(data.isRepresentative);
117-
setTimeAgo(dayjs(data.createdAt).locale('ko').fromNow());
118-
} catch (error) {
119-
console.error('Error fetching post data:', error);
120-
}
121-
};
122-
123-
getPost();
124-
}, [postId]);
84+
const { mutate: togglePostLikeStatus } = useMutation({
85+
mutationFn: () => togglePostLikeStatusApi(Number(postId)),
86+
onSuccess: () => {
87+
queryClient.setQueryData(['postDetail', Number(postId)], (oldData: GetPostDetailResponse | undefined) => {
88+
if (!oldData) return oldData;
89+
90+
const newData = {
91+
...oldData,
92+
data: {
93+
...oldData.data,
94+
postLikesCount: oldData.data.postLikesCount + (oldData.data.isPostLike ? -1 : 1), // 기존 좋아요 개수를 토대로 증가/감소
95+
isPostLike: !oldData.data.isPostLike, // 좋아요 상태 변경
96+
},
97+
};
98+
console.log('newData', newData);
99+
100+
return newData;
101+
});
102+
},
103+
});
125104

126105
useEffect(() => {
127106
if (contentRef.current) {
@@ -145,83 +124,96 @@ const PostBase: React.FC<PostBaseProps> = ({ onClickMenu }) => {
145124
},
146125
};
147126

148-
return (
149-
<OODDFrame>
150-
<TopBar LeftButtonSrc={Left} />
151-
152-
<PostLayout>
153-
<PostContainer>
127+
if (isLoading) {
128+
return (
129+
<OODDFrame>
130+
<TopBar LeftButtonSrc={Left} onClickLeftButton={() => nav(-1)} />
131+
<PostLayout>
154132
<PostInfoContainer>
155-
<UserProfile onClick={handleUserClick}>
156-
{post?.user && <img src={post.user.profilePictureUrl} alt="profileImg" />}
133+
<UserProfile>
134+
<Skeleton width={2.5} height={2.5} borderRadius={2.5} />
157135
</UserProfile>
158-
<UserName
159-
onClick={handleUserClick}
160-
$textTheme={{ style: 'body2-medium' }}
161-
color={theme.colors.text.primary}
162-
>
163-
{user.nickname}
164-
</UserName>
165-
<StyledText
166-
className="timeAgo"
167-
$textTheme={{ style: 'caption2-regular' }}
168-
color={theme.colors.text.tertiary}
169-
>
170-
{timeAgo}
171-
</StyledText>
172-
<MenuBtn onClick={onClickMenu}>
173-
<img src={More} alt="more" />
174-
</MenuBtn>
136+
<UserNameWrapper>
137+
<Skeleton width={6.25} height={1.25} />
138+
</UserNameWrapper>
175139
</PostInfoContainer>
140+
<PostWrapper>
141+
<Skeleton width="100%" height={40} />
142+
</PostWrapper>
143+
</PostLayout>
144+
</OODDFrame>
145+
);
146+
}
176147

177-
{!post ? <ImageSkeleton /> : <ImageSwiper images={post.postImages.map((image) => image.url)} />}
148+
return (
149+
<OODDFrame>
150+
<TopBar LeftButtonSrc={Left} />
178151

179-
{post?.postClothings && (
180-
<ClothingInfoList className="post-mode">
181-
{post.postClothings.map((clothingObj, index) => (
182-
<ClothingInfoItem key={index} clothingObj={clothingObj} />
183-
))}
184-
</ClothingInfoList>
152+
<PostLayout>
153+
<PostInfoContainer>
154+
<UserProfile onClick={handleUserClick}>
155+
{user && <img src={post.user.profilePictureUrl} alt="profileImg" />}
156+
</UserProfile>
157+
<UserName onClick={handleUserClick} $textTheme={{ style: 'body2-medium' }} color={theme.colors.text.primary}>
158+
{user?.nickname ?? '알수없음'}
159+
</UserName>
160+
<StyledText className="timeAgo" $textTheme={{ style: 'caption2-regular' }} color={theme.colors.text.tertiary}>
161+
{timeAgo}
162+
</StyledText>
163+
<MenuBtn onClick={onClickMenu}>
164+
<img src={More} alt="more" />
165+
</MenuBtn>
166+
</PostInfoContainer>
167+
168+
{post && (
169+
<PostWrapper>
170+
<ImageSwiper images={post.postImages.map((image) => image.url)} />
171+
</PostWrapper>
172+
)}
173+
174+
{post?.postClothings ? (
175+
<ClothingInfoList className="post-mode">
176+
{post.postClothings.map((clothingObj, index) => (
177+
<ClothingInfoItem key={index} clothingObj={clothingObj} />
178+
))}
179+
</ClothingInfoList>
180+
) : null}
181+
182+
<IconRow>
183+
<IconWrapper>
184+
<Icon onClick={() => togglePostLikeStatus()}>
185+
{post?.isPostLike ? <Like isFilled={true} color={theme.colors.brand.primary} /> : <Like />}
186+
</Icon>
187+
<span onClick={() => handleLikeCommentOpen('likes')}>{post?.postLikesCount ?? 0}</span>
188+
</IconWrapper>
189+
<IconWrapper onClick={() => handleLikeCommentOpen('comments')}>
190+
<Icon>
191+
<img src={Message} alt="message" />
192+
</Icon>
193+
<span>{post?.postCommentsCount ?? 0}</span>
194+
</IconWrapper>
195+
</IconRow>
196+
197+
<PostContentContainer>
198+
{post && (
199+
<div>
200+
<Content
201+
ref={contentRef}
202+
onClick={toggleTextDisplay}
203+
$showFullText={showFullText}
204+
$textTheme={{ style: 'body4-light' }}
205+
color={theme.colors.text.primary}
206+
>
207+
{post.content}
208+
</Content>
209+
{isTextOverflowing && (
210+
<ShowMoreButton onClick={toggleTextDisplay} $textTheme={{ style: 'body4-light' }}>
211+
{showFullText ? '간략히 보기' : '더 보기'}
212+
</ShowMoreButton>
213+
)}
214+
</div>
185215
)}
186-
187-
<IconRow>
188-
<IconWrapper>
189-
<Icon onClick={togglePostLikeStatus}>
190-
{post?.isPostLike ? <Like isFilled={true} color={theme.colors.brand.primary} /> : <Like />}
191-
</Icon>
192-
<span onClick={() => handleLikeCommentOpen('likes')}>{post?.postLikesCount ?? 0}</span>
193-
</IconWrapper>
194-
<IconWrapper onClick={() => handleLikeCommentOpen('comments')}>
195-
<Icon>
196-
<img src={Message} alt="message" />
197-
</Icon>
198-
<span>{post?.postCommentsCount ?? 0}</span>
199-
</IconWrapper>
200-
</IconRow>
201-
202-
<PostContentContainer>
203-
{!post ? (
204-
<ContentSkeleton />
205-
) : (
206-
<div>
207-
<Content
208-
ref={contentRef}
209-
onClick={toggleTextDisplay}
210-
$showFullText={showFullText}
211-
$textTheme={{ style: 'body4-light' }}
212-
color={theme.colors.text.primary}
213-
>
214-
{post.content}
215-
</Content>
216-
{isTextOverflowing && (
217-
<ShowMoreButton onClick={toggleTextDisplay} $textTheme={{ style: 'body4-light' }}>
218-
{showFullText ? '간략히 보기' : '더 보기'}
219-
</ShowMoreButton>
220-
)}
221-
</div>
222-
)}
223-
</PostContentContainer>
224-
</PostContainer>
216+
</PostContentContainer>
225217
</PostLayout>
226218

227219
<NavBar />

0 commit comments

Comments
 (0)