11import { useEffect , useState , useRef } from 'react' ;
22import { 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
88import theme from '@styles/theme' ;
99
10- import { getPostDetailApi } from '@apis/post' ;
10+ import { usePostDetail } from '@apis/post' ;
1111import { togglePostLikeStatusApi } from '@apis/post-like' ;
12- import { postIdAtom , userAtom , isPostRepresentativeAtom } from '@recoil/Post/PostAtom' ;
1312
1413import Left from '@assets/arrow/left.svg' ;
1514import Message from '@assets/default/message.svg' ;
@@ -21,6 +20,7 @@ import BottomSheet from '@components/BottomSheet';
2120import ClothingInfoItem from '@components/ClothingInfoItem' ;
2221import { OODDFrame } from '@components/Frame/Frame' ;
2322import NavBar from '@components/NavBar' ;
23+ import Skeleton from '@components/Skeleton' ;
2424import { StyledText } from '@components/Text/StyledText' ;
2525import TopBar from '@components/TopBar' ;
2626
@@ -34,28 +34,23 @@ import LikeCommentBottomSheetContent from './LikeCommentBottomSheetContent/index
3434
3535import {
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
5352const 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