diff --git a/src/App.tsx b/src/App.tsx index 57a96b0..29e7dfa 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -18,6 +18,7 @@ import LetterBoxDetailPage from './pages/LetterBoxDetail'; import LetterDetailPage from './pages/LetterDetail'; import LoginPage from './pages/Login'; import MyPage from './pages/MyPage'; +import MyBoardPage from './pages/MyPage/components/MyBoardPage'; import NotFoundPage from './pages/NotFound'; import NotificationsPage from './pages/Notifications'; import OnboardingPage from './pages/Onboarding'; @@ -57,7 +58,7 @@ const App = () => { }> } /> - } /> + } /> } /> diff --git a/src/apis/auth.ts b/src/apis/auth.ts index 9105f60..064fa5d 100644 --- a/src/apis/auth.ts +++ b/src/apis/auth.ts @@ -63,10 +63,15 @@ export const deleteUserInfo = async () => { export const postLogout = async () => { try { + console.log(' before logout'); + const response = await client.post('/api/logout', { withCredentials: true }); + console.log('logout', response); if (!response) throw new Error('postLogout: failed to logout'); return response; } catch (error) { + console.log('logout error'); + console.error(error); } }; diff --git a/src/apis/client.ts b/src/apis/client.ts index 1a4f1e2..df8b3b3 100644 --- a/src/apis/client.ts +++ b/src/apis/client.ts @@ -9,27 +9,27 @@ const client = axios.create({ headers: { 'Content-Type': 'application/json' }, }); -type FailedRequest = { - resolve: (token: string) => void; - reject: (error: unknown) => void; -}; +// type FailedRequest = { +// resolve: (token: string) => void; +// reject: (error: unknown) => void; +// }; let isRefreshing = false; -let failedQueue: FailedRequest[] = []; - -const processQueue = (error: unknown, token: string | null = null) => { - failedQueue.forEach((prom) => { - if (error) { - prom.reject(error); - } else { - if (token) { - prom.resolve(token); - } - } - }); - - failedQueue = []; -}; +// let failedQueue: FailedRequest[] = []; + +// const processQueue = (error: unknown, token: string | null = null) => { +// failedQueue.forEach((prom) => { +// if (error) { +// prom.reject(error); +// } else { +// if (token) { +// prom.resolve(token); +// } +// } +// }); + +// failedQueue = []; +// }; const callReissue = async () => { try { @@ -41,6 +41,8 @@ const callReissue = async () => { } }; +let retry = false; + client.interceptors.request.use( (config) => { console.log('response again', config); @@ -68,43 +70,42 @@ client.interceptors.response.use( return Promise.reject(error); } - if ( - (error.response?.status === 401 || error.response?.status === 403) && - !originalRequest._retry - ) { - originalRequest._retry = true; - + if ((error.response?.status === 401 || error.response?.status === 403) && !retry) { + retry = true; if (isRefreshing) { - try { - return new Promise((resolve, reject) => { - failedQueue.push({ - resolve: (token: string) => { - originalRequest.headers.Authorization = `Bearer ${token}`; - resolve(client(originalRequest)); - }, - reject: (err: unknown) => reject(err), - }); - }); - } catch (e) { - return Promise.reject(e); - } + if (isLoggedIn) logout(); + // try { + // return new Promise((resolve, reject) => { + // failedQueue.push({ + // resolve: (token: string) => { + // originalRequest.headers.Authorization = `Bearer ${token}`; + // resolve(client(originalRequest)); + // }, + // reject: (err: unknown) => reject(err), + // }); + // }); + // } catch (e) { + // return Promise.reject(e); + // } } else { isRefreshing = true; try { const newToken = await callReissue(); setAccessToken(newToken); - processQueue(null, newToken); + // processQueue(null, newToken); isRefreshing = false; originalRequest.headers.Authorization = `Bearer ${newToken}`; return client(originalRequest); } catch (e) { - processQueue(e, null); + // processQueue(e, null); isRefreshing = false; if (isLoggedIn) logout(); return Promise.reject(e); } } } + if (isLoggedIn) logout(); + console.error('Failed to refresh token', error); return Promise.reject(error); }, ); diff --git a/src/apis/myPage.ts b/src/apis/myPage.ts index 6e5057e..7d64cc9 100644 --- a/src/apis/myPage.ts +++ b/src/apis/myPage.ts @@ -9,3 +9,13 @@ export const fetchMyPageInfo = async () => { console.error(error); } }; + +export const getMySharePostList = async () => { + try { + const response = await client.get('/api/share-proposals/inbox'); + if (!response) throw new Error('error while fetching my share post list'); + return response.data; + } catch (error) { + console.error(error); + } +}; diff --git a/src/apis/share.ts b/src/apis/share.ts index d9edab3..767b97e 100644 --- a/src/apis/share.ts +++ b/src/apis/share.ts @@ -38,10 +38,7 @@ export interface SharePostApproval { } // 공유 게시글 목록 조회 -export const getSharePostList = async ( - page: number = 1, - size: number = 10, -): Promise => { +export const getSharePostList = async (page: number = 1, size: number = 10) => { try { const response = await client.get('/api/share-posts', { params: { page, size }, @@ -70,14 +67,12 @@ export const getSharePostDetail = async (sharePostId: number): Promise { try { const response = await client.post('/api/share-proposals', { letterIds: letterIds, - requesterId, recipientId, message, }); @@ -105,3 +100,27 @@ export const postShareProposalApproval = async ( throw new Error(`편지 공유 ${action === 'approve' ? '수락' : '거부'} 실패`); } }; + +// 편지 좋아요 추가, 취소 +export const postSharePostLike = async (sharePostId: number) => { + try { + const response = await client.post(`/api/share-posts/${sharePostId}/likes`); + if (!response) throw new Error('error while posting like'); + return response.data; + } catch (error) { + console.error('❌ 편지 좋아요 중 에러가 발생했습니다', error); + throw new Error('편지 좋아요 실패'); + } +}; + +// 편지 좋아요 갯수 +export const getSharePostLikeCount = async (sharePostId: number) => { + try { + const response = await client.get(`/api/share-posts/${sharePostId}/likes`); + if (!response) throw new Error('error while fetching likes'); + return response.data; + } catch (error) { + console.error('❌ 편지 좋아요 갯수 조회 중 에러가 발생했습니다', error); + throw new Error('편지 좋아요 갯수 조회 실패'); + } +}; diff --git a/src/layouts/Header.tsx b/src/layouts/Header.tsx index 53af0c8..8efa549 100644 --- a/src/layouts/Header.tsx +++ b/src/layouts/Header.tsx @@ -15,7 +15,9 @@ const Header = () => { - + + + ); diff --git a/src/pages/LetterBoard/components/LetterPreview.tsx b/src/pages/LetterBoard/components/LetterPreview.tsx index e980e5e..2546caa 100644 --- a/src/pages/LetterBoard/components/LetterPreview.tsx +++ b/src/pages/LetterBoard/components/LetterPreview.tsx @@ -1,26 +1,27 @@ -import { Link } from 'react-router'; +import { forwardRef } from 'react'; +import { useNavigate } from 'react-router'; import LetterWrapper from '@/components/LetterWrapper'; interface LetterPreviewProps { - id: string; + id: number; to: string; from: string; content: string; } -const LetterPreview = ({ id, to, from, content }: LetterPreviewProps) => { +const LetterPreview = forwardRef((props, ref) => { + const { id, to, from, content }: LetterPreviewProps = props; + const navigate = useNavigate(); return ( - - -
-

From.{from}

-

{content}

-

To.{to}

-
-
- + navigate(`/board/letter/${id}`)}> +
+

From.{from}

+

{content}

+

To.{to}

+
+
); -}; +}); export default LetterPreview; diff --git a/src/pages/LetterBoard/index.tsx b/src/pages/LetterBoard/index.tsx index 4c5739b..80d26a7 100644 --- a/src/pages/LetterBoard/index.tsx +++ b/src/pages/LetterBoard/index.tsx @@ -1,5 +1,9 @@ -import { twMerge } from 'tailwind-merge'; +import { useInfiniteQuery } from '@tanstack/react-query'; +import { useEffect } from 'react'; +import { useInView } from 'react-intersection-observer'; +import { useNavigate } from 'react-router'; +import { getSharePostList } from '@/apis/share'; import BackgroundBottom from '@/components/BackgroundBottom'; import NoticeRollingPaper from '@/components/NoticeRollingPaper'; import PageTitle from '@/components/PageTitle'; @@ -7,34 +11,77 @@ import PageTitle from '@/components/PageTitle'; import LetterPreview from './components/LetterPreview'; const LetterBoardPage = () => { - const isMyBoard = window.location.pathname.includes('/mypage'); + const navigate = useNavigate(); + const { ref, inView } = useInView(); + + const fetchPostList = async (page: number = 1) => { + try { + const response = await getSharePostList(page); + if (!response) throw new Error('게시글 목록을 불러오는데 실패했습니다.'); + console.log('page', response); + return response as SharePostResponse; + } catch (e) { + console.error(e); + } + }; + + const { data, isLoading, isError, fetchNextPage, hasNextPage, isFetchingNextPage } = + useInfiniteQuery({ + queryKey: ['sharePostList'], + queryFn: ({ pageParam = 1 }) => fetchPostList(pageParam), + enabled: true, + initialPageParam: 1, + getNextPageParam: (res) => { + if (!res || res.currentPage >= res.totalPages) { + return undefined; + } + return res.currentPage + 1; + }, + staleTime: 1000 * 60 * 5, + gcTime: 1000 * 60 * 10, + }); + + const postLists = data?.pages.flatMap((page) => page?.content) || []; + + useEffect(() => { + if (!hasNextPage) return; + if (inView && !isFetchingNextPage) { + fetchNextPage(); + } + }, [inView, hasNextPage, isFetchingNextPage, fetchNextPage]); + + if (isError) { + navigate('/notFound'); + } return ( <> -
- {isMyBoard ? ( - 내가 올린 게시물 +
+ <> + + 게시판 +

+ 따숨이에게 힘이 되었던 다양한 편지들을 모아두었어요 +

+ + {isLoading ? ( +

loading

) : ( - <> - - 게시판 -

- 따숨이에게 힘이 되었던 다양한 편지들을 모아두었어요 -

- +
+ {postLists.map((item, index) => { + return ( + + ); + })} +
)} -
- {Array.from({ length: 10 }).map((_, index) => ( - - ))} -
diff --git a/src/pages/LetterBoardDetail/index.tsx b/src/pages/LetterBoardDetail/index.tsx index 7d60ea1..3f8292d 100644 --- a/src/pages/LetterBoardDetail/index.tsx +++ b/src/pages/LetterBoardDetail/index.tsx @@ -2,7 +2,13 @@ import { useEffect, useState } from 'react'; import { useLocation, useNavigate } from 'react-router'; import { twMerge } from 'tailwind-merge'; -import { getSharePostDetail, postShareProposalApproval, SharePost } from '@/apis/share'; +import { + getSharePostDetail, + postShareProposalApproval, + SharePost, + postSharePostLike, + getSharePostLikeCount, +} from '@/apis/share'; import BlurImg from '@/assets/images/landing-blur.png'; import ReportModal from '@/components/ReportModal'; @@ -32,27 +38,37 @@ const LetterBoardDetailPage = ({ confirmDisabled }: ShareLetterPreviewProps) => const [postDetail, setPostDetail] = useState(); useEffect(() => { - const fetchPostDetail = async () => { - console.log('location.state:', location.state); - + const { sharePostId } = location.state.postDetail; + const fetchPostDetail = async (postId: number) => { try { - if (location.state?.postDetail) { - const { sharePostId } = location.state.postDetail; - - console.log('sharePostId:', sharePostId); + console.log('sharePostId:', postId); - const data = await getSharePostDetail(sharePostId); + const data = await getSharePostDetail(postId); - setPostDetail(data); - } else { - console.warn('postDetail not found in location.state'); - } + setPostDetail(data); } catch (error) { console.error('❌ 공유 게시글 상세 조회에 실패했습니다.', error); } }; - fetchPostDetail(); + const fetchLikeCounts = async (postId: number) => { + try { + const response = await getSharePostLikeCount(postId); + if (!response) throw new Error('error while fetching like count'); + console.log(response); + setLikeCount(response.data.likeCount); + } catch (error) { + console.error('❌ 편지 좋아요 갯수를 가져오는 중 에러가 발생했습니다', error); + throw new Error('편지 좋아요 갯수 가져오기 실패'); + } + }; + + if (location.state?.postDetail) { + fetchPostDetail(sharePostId); + fetchLikeCounts(sharePostId); + } else { + console.warn('postDetail not found in location.state'); + } }, [location.state]); const handleProposalApproval = async ( diff --git a/src/pages/LetterBoxDetail/index.tsx b/src/pages/LetterBoxDetail/index.tsx index e2b7097..262cc03 100644 --- a/src/pages/LetterBoxDetail/index.tsx +++ b/src/pages/LetterBoxDetail/index.tsx @@ -72,7 +72,6 @@ const LetterBoxDetailPage = () => { }); const shareMutation = useMutation({ - // Todo : useAuthStore -> myId 대체 mutationFn: () => postShareProposals(selected, userInfo.id, shareComment), onSuccess: () => { toggleShareMode(); diff --git a/src/pages/MyPage/components/MyBoardPage.tsx b/src/pages/MyPage/components/MyBoardPage.tsx new file mode 100644 index 0000000..388934f --- /dev/null +++ b/src/pages/MyPage/components/MyBoardPage.tsx @@ -0,0 +1,66 @@ +import { useQuery } from '@tanstack/react-query'; +import { useNavigate } from 'react-router'; +import { twMerge } from 'tailwind-merge'; + +import { getMySharePostList } from '@/apis/mypage'; +import BackgroundBottom from '@/components/BackgroundBottom'; +import PageTitle from '@/components/PageTitle'; + +import LetterPreview from '../../LetterBoard/components/LetterPreview'; + +const MyBoardPage = () => { + const navigate = useNavigate(); + + const fetchMyPostList = async () => { + try { + const response = await getMySharePostList(); + if (!response) throw new Error('게시글 목록을 불러오는데 실패했습니다.'); + console.log(response); + return response.data; + } catch (e) { + console.error(e); + } + }; + + const { + data: postLists = [], + isLoading, + isError, + } = useQuery({ + queryKey: ['sharePostList'], + queryFn: () => fetchMyPostList(), + enabled: true, + staleTime: 1000 * 60 * 5, + gcTime: 1000 * 60 * 10, + }); + + if (isError) { + navigate('/notFound'); + } + return ( + <> +
+ 내가 올린 게시물 + {isLoading ? ( +

loading

+ ) : ( +
+ {postLists.map((item, index) => ( + + ))} +
+ )} +
+ + + ); +}; + +export default MyBoardPage; diff --git a/src/pages/MyPage/index.tsx b/src/pages/MyPage/index.tsx index 4c00707..2ed0b5c 100644 --- a/src/pages/MyPage/index.tsx +++ b/src/pages/MyPage/index.tsx @@ -81,9 +81,13 @@ const MyPage = () => {

고객 센터

- -

운영자에게 문의하기

- + + 운영자에게 문의하기 +

계정

diff --git a/src/stores/authStore.ts b/src/stores/authStore.ts index 6c881cc..a0b56c1 100644 --- a/src/stores/authStore.ts +++ b/src/stores/authStore.ts @@ -1,7 +1,7 @@ import { create } from 'zustand'; import { persist, createJSONStorage } from 'zustand/middleware'; -import { postLogout } from '@/apis/auth'; +// import { postLogout } from '@/apis/auth'; interface AuthStore { isLoggedIn: boolean; @@ -21,12 +21,13 @@ const useAuthStore = create( zipCode: '', login: () => set({ isLoggedIn: true }), logout: async () => { - try { - await postLogout(); - } catch (e) { - console.error(e); - } set({ isLoggedIn: false, zipCode: '', accessToken: '' }); + // location.reload(); + // try { + // await postLogout(); + // } catch (e) { + // console.error(e); + // } }, setZipCode: (zipCode) => set({ zipCode: zipCode }), setAccessToken: (accessToken) => set({ accessToken: accessToken }), diff --git a/src/types/share.d.ts b/src/types/share.d.ts new file mode 100644 index 0000000..df79d3b --- /dev/null +++ b/src/types/share.d.ts @@ -0,0 +1,29 @@ +//공유 게시글 상세 페이지 편지 +interface ShareLetter { + id: number; + content: string; + writerZipCode: string; + receiverZipCode: string; +} + +// 공유 게시글 목록 조회 타입 +interface SharePost { + writerZipCode: string; + receiverZipCode: string; + content: string; + createdAt: string; + active: boolean; + sharePostId: number; + sharePostContent: string; + letters: ShareLetter[]; +} + +// 페이징 포함 +interface SharePostResponse { +// data: any; + content: SharePost[]; + currentPage: number; + size: number; + totalElements: number; + totalPages: number; +}