Skip to content

Commit cbe92b0

Browse files
nirii00tifsy
andauthored
feat: 게시판 1차 기능구현 (#76)
* fix: 토큰 만료로 reissue 실패시 로그아웃 안되는 문제 해결 * fix: reissue 에러 시 에러 처리 안되는 문제 해결 - client.ts의 interceptors.response.use 구문 내에서 api 호출및 데이터 가공을 함수 밖으로 꺼냄 * fix: mailbox에서 isClosed 옵션 반대로 보여주는 문제 해결 - isClosed 상태를 반대로 받아와서 상태를 잘못 보여주는 문제 해결 * fix: 401 에러가 아닌 경우 바로 로그아웃 되는 문제 해결 * FIx: format date 로직 수정 - 소숫점 이하 자리로 인해 에러가 날 경우가 있을 것 같아서 . 기준 split 추가 * fix: mailBox 배포 api에 따른 수정 작업 - sharePost 요청 요청자 id 삭제 - 상세 페이지 api 경로 수정 - 우편함 상세체이지 날짜 잘못표기하는 에러 수정 * refactor: PR 리뷰를 반영한 리팩토링 (#70) * refactor: 읽지 않은 편지 수 조회 기능 리팩토링 * rename: NewLetterModal 을 UnreadLetterModal로 파일명 및 함수명 변경 * fix: 읽지 않은 편지가 존재할 때 다른 우체통 이미지 보여주도록 수정 * fix: 오고 있는 편지 모달에서 편지 항목 눌렀을 때 보드로 이동하는 에러 해결 * fix: 게시판 편지 공유 모달 오류 해결 (#73) * fix: ShowShareAccessModal에서 undefined 배열 map 호출 오류 해결 * fix: 공유 상세 보기 페이지에서 발신자 데이터 바인딩 문제 해결 * fix: 글 작성자 배경색이 적용되지 않는 오류 해결 * design: 공유 상세 보기 페이지 하단 블러 이미지 위치 조정 * fix: 토큰 만료로 reissue 실패시 로그아웃 안되는 문제 해결 * fix: reissue 에러 시 에러 처리 안되는 문제 해결 - client.ts의 interceptors.response.use 구문 내에서 api 호출및 데이터 가공을 함수 밖으로 꺼냄 * fix: mailbox에서 isClosed 옵션 반대로 보여주는 문제 해결 - isClosed 상태를 반대로 받아와서 상태를 잘못 보여주는 문제 해결 * fix: mailBox 배포 api에 따른 수정 작업 - sharePost 요청 요청자 id 삭제 - 상세 페이지 api 경로 수정 - 우편함 상세체이지 날짜 잘못표기하는 에러 수정 * feat: myPage 데이터 바인딩 수정, api 추가 * fix: 로그인 reissue 로직 수정 - reissue 401에러 시에 진행 안되는 문제 해결 * fix: 무한 요청 에러 수정 - retry flag의 위치 변경 * feat: 게시판 데이터 바인딩 - 게시판에 무한 스크롤 반영 - 게시판 데이터 바인딩 - myPage와 게시판 화면 분리 - header에 myPage 링크 --------- Co-authored-by: nirii00 <[email protected]> Co-authored-by: Seungyeon Han (Tiffany) <[email protected]>
1 parent 9812d33 commit cbe92b0

File tree

14 files changed

+310
-109
lines changed

14 files changed

+310
-109
lines changed

src/App.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import LetterBoxDetailPage from './pages/LetterBoxDetail';
1818
import LetterDetailPage from './pages/LetterDetail';
1919
import LoginPage from './pages/Login';
2020
import MyPage from './pages/MyPage';
21+
import MyBoardPage from './pages/MyPage/components/MyBoardPage';
2122
import NotFoundPage from './pages/NotFound';
2223
import NotificationsPage from './pages/Notifications';
2324
import OnboardingPage from './pages/Onboarding';
@@ -57,7 +58,7 @@ const App = () => {
5758
</Route>
5859
<Route path="mypage" element={<Layout />}>
5960
<Route index element={<MyPage />} />
60-
<Route path="board" element={<LetterBoardPage />} />
61+
<Route path="board" element={<MyBoardPage />} />
6162
<Route path="notifications" element={<NotificationsPage />} />
6263
</Route>
6364
</Route>

src/apis/auth.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,10 +63,15 @@ export const deleteUserInfo = async () => {
6363

6464
export const postLogout = async () => {
6565
try {
66+
console.log(' before logout');
67+
6668
const response = await client.post('/api/logout', { withCredentials: true });
69+
console.log('logout', response);
6770
if (!response) throw new Error('postLogout: failed to logout');
6871
return response;
6972
} catch (error) {
73+
console.log('logout error');
74+
7075
console.error(error);
7176
}
7277
};

src/apis/client.ts

Lines changed: 41 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -9,27 +9,27 @@ const client = axios.create({
99
headers: { 'Content-Type': 'application/json' },
1010
});
1111

12-
type FailedRequest = {
13-
resolve: (token: string) => void;
14-
reject: (error: unknown) => void;
15-
};
12+
// type FailedRequest = {
13+
// resolve: (token: string) => void;
14+
// reject: (error: unknown) => void;
15+
// };
1616

1717
let isRefreshing = false;
18-
let failedQueue: FailedRequest[] = [];
19-
20-
const processQueue = (error: unknown, token: string | null = null) => {
21-
failedQueue.forEach((prom) => {
22-
if (error) {
23-
prom.reject(error);
24-
} else {
25-
if (token) {
26-
prom.resolve(token);
27-
}
28-
}
29-
});
30-
31-
failedQueue = [];
32-
};
18+
// let failedQueue: FailedRequest[] = [];
19+
20+
// const processQueue = (error: unknown, token: string | null = null) => {
21+
// failedQueue.forEach((prom) => {
22+
// if (error) {
23+
// prom.reject(error);
24+
// } else {
25+
// if (token) {
26+
// prom.resolve(token);
27+
// }
28+
// }
29+
// });
30+
31+
// failedQueue = [];
32+
// };
3333

3434
const callReissue = async () => {
3535
try {
@@ -41,6 +41,8 @@ const callReissue = async () => {
4141
}
4242
};
4343

44+
let retry = false;
45+
4446
client.interceptors.request.use(
4547
(config) => {
4648
console.log('response again', config);
@@ -68,43 +70,42 @@ client.interceptors.response.use(
6870
return Promise.reject(error);
6971
}
7072

71-
if (
72-
(error.response?.status === 401 || error.response?.status === 403) &&
73-
!originalRequest._retry
74-
) {
75-
originalRequest._retry = true;
76-
73+
if ((error.response?.status === 401 || error.response?.status === 403) && !retry) {
74+
retry = true;
7775
if (isRefreshing) {
78-
try {
79-
return new Promise((resolve, reject) => {
80-
failedQueue.push({
81-
resolve: (token: string) => {
82-
originalRequest.headers.Authorization = `Bearer ${token}`;
83-
resolve(client(originalRequest));
84-
},
85-
reject: (err: unknown) => reject(err),
86-
});
87-
});
88-
} catch (e) {
89-
return Promise.reject(e);
90-
}
76+
if (isLoggedIn) logout();
77+
// try {
78+
// return new Promise((resolve, reject) => {
79+
// failedQueue.push({
80+
// resolve: (token: string) => {
81+
// originalRequest.headers.Authorization = `Bearer ${token}`;
82+
// resolve(client(originalRequest));
83+
// },
84+
// reject: (err: unknown) => reject(err),
85+
// });
86+
// });
87+
// } catch (e) {
88+
// return Promise.reject(e);
89+
// }
9190
} else {
9291
isRefreshing = true;
9392
try {
9493
const newToken = await callReissue();
9594
setAccessToken(newToken);
96-
processQueue(null, newToken);
95+
// processQueue(null, newToken);
9796
isRefreshing = false;
9897
originalRequest.headers.Authorization = `Bearer ${newToken}`;
9998
return client(originalRequest);
10099
} catch (e) {
101-
processQueue(e, null);
100+
// processQueue(e, null);
102101
isRefreshing = false;
103102
if (isLoggedIn) logout();
104103
return Promise.reject(e);
105104
}
106105
}
107106
}
107+
if (isLoggedIn) logout();
108+
console.error('Failed to refresh token', error);
108109
return Promise.reject(error);
109110
},
110111
);

src/apis/myPage.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,13 @@ export const fetchMyPageInfo = async () => {
99
console.error(error);
1010
}
1111
};
12+
13+
export const getMySharePostList = async () => {
14+
try {
15+
const response = await client.get('/api/share-proposals/inbox');
16+
if (!response) throw new Error('error while fetching my share post list');
17+
return response.data;
18+
} catch (error) {
19+
console.error(error);
20+
}
21+
};

src/apis/share.ts

Lines changed: 25 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -38,10 +38,7 @@ export interface SharePostApproval {
3838
}
3939

4040
// 공유 게시글 목록 조회
41-
export const getSharePostList = async (
42-
page: number = 1,
43-
size: number = 10,
44-
): Promise<SharePostResponse> => {
41+
export const getSharePostList = async (page: number = 1, size: number = 10) => {
4542
try {
4643
const response = await client.get('/api/share-posts', {
4744
params: { page, size },
@@ -70,14 +67,12 @@ export const getSharePostDetail = async (sharePostId: number): Promise<SharePost
7067
// 공유 요청 보내기
7168
export const postShareProposals = async (
7269
letterIds: number[],
73-
requesterId: number,
7470
recipientId: number,
7571
message: string,
7672
) => {
7773
try {
7874
const response = await client.post('/api/share-proposals', {
7975
letterIds: letterIds,
80-
requesterId,
8176
recipientId,
8277
message,
8378
});
@@ -105,3 +100,27 @@ export const postShareProposalApproval = async (
105100
throw new Error(`편지 공유 ${action === 'approve' ? '수락' : '거부'} 실패`);
106101
}
107102
};
103+
104+
// 편지 좋아요 추가, 취소
105+
export const postSharePostLike = async (sharePostId: number) => {
106+
try {
107+
const response = await client.post(`/api/share-posts/${sharePostId}/likes`);
108+
if (!response) throw new Error('error while posting like');
109+
return response.data;
110+
} catch (error) {
111+
console.error('❌ 편지 좋아요 중 에러가 발생했습니다', error);
112+
throw new Error('편지 좋아요 실패');
113+
}
114+
};
115+
116+
// 편지 좋아요 갯수
117+
export const getSharePostLikeCount = async (sharePostId: number) => {
118+
try {
119+
const response = await client.get(`/api/share-posts/${sharePostId}/likes`);
120+
if (!response) throw new Error('error while fetching likes');
121+
return response.data;
122+
} catch (error) {
123+
console.error('❌ 편지 좋아요 갯수 조회 중 에러가 발생했습니다', error);
124+
throw new Error('편지 좋아요 갯수 조회 실패');
125+
}
126+
};

src/layouts/Header.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,9 @@ const Header = () => {
1515
<Link to="/mypage/notifications">
1616
<AlarmIcon className="h-6 w-6 text-white" />
1717
</Link>
18-
<PersonIcon className="h-6 w-6 text-white" />
18+
<Link to="/mypage">
19+
<PersonIcon className="h-6 w-6 text-white" />
20+
</Link>
1921
</div>
2022
</header>
2123
);
Lines changed: 14 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,27 @@
1-
import { Link } from 'react-router';
1+
import { forwardRef } from 'react';
2+
import { useNavigate } from 'react-router';
23

34
import LetterWrapper from '@/components/LetterWrapper';
45

56
interface LetterPreviewProps {
6-
id: string;
7+
id: number;
78
to: string;
89
from: string;
910
content: string;
1011
}
1112

12-
const LetterPreview = ({ id, to, from, content }: LetterPreviewProps) => {
13+
const LetterPreview = forwardRef<HTMLDivElement, LetterPreviewProps>((props, ref) => {
14+
const { id, to, from, content }: LetterPreviewProps = props;
15+
const navigate = useNavigate();
1316
return (
14-
<Link to={id}>
15-
<LetterWrapper className="px-3 py-2">
16-
<div className="caption-r flex flex-col gap-2">
17-
<p>From.{from}</p>
18-
<p className="line-clamp-2 font-light">{content}</p>
19-
<p className="place-self-end">To.{to}</p>
20-
</div>
21-
</LetterWrapper>
22-
</Link>
17+
<LetterWrapper className="px-3 py-2" ref={ref} onClick={() => navigate(`/board/letter/${id}`)}>
18+
<div className="caption-r flex flex-col gap-2">
19+
<p>From.{from}</p>
20+
<p className="line-clamp-2 font-light">{content}</p>
21+
<p className="place-self-end">To.{to}</p>
22+
</div>
23+
</LetterWrapper>
2324
);
24-
};
25+
});
2526

2627
export default LetterPreview;

src/pages/LetterBoard/index.tsx

Lines changed: 71 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,40 +1,87 @@
1-
import { twMerge } from 'tailwind-merge';
1+
import { useInfiniteQuery } from '@tanstack/react-query';
2+
import { useEffect } from 'react';
3+
import { useInView } from 'react-intersection-observer';
4+
import { useNavigate } from 'react-router';
25

6+
import { getSharePostList } from '@/apis/share';
37
import BackgroundBottom from '@/components/BackgroundBottom';
48
import NoticeRollingPaper from '@/components/NoticeRollingPaper';
59
import PageTitle from '@/components/PageTitle';
610

711
import LetterPreview from './components/LetterPreview';
812

913
const LetterBoardPage = () => {
10-
const isMyBoard = window.location.pathname.includes('/mypage');
14+
const navigate = useNavigate();
15+
const { ref, inView } = useInView();
16+
17+
const fetchPostList = async (page: number = 1) => {
18+
try {
19+
const response = await getSharePostList(page);
20+
if (!response) throw new Error('게시글 목록을 불러오는데 실패했습니다.');
21+
console.log('page', response);
22+
return response as SharePostResponse;
23+
} catch (e) {
24+
console.error(e);
25+
}
26+
};
27+
28+
const { data, isLoading, isError, fetchNextPage, hasNextPage, isFetchingNextPage } =
29+
useInfiniteQuery({
30+
queryKey: ['sharePostList'],
31+
queryFn: ({ pageParam = 1 }) => fetchPostList(pageParam),
32+
enabled: true,
33+
initialPageParam: 1,
34+
getNextPageParam: (res) => {
35+
if (!res || res.currentPage >= res.totalPages) {
36+
return undefined;
37+
}
38+
return res.currentPage + 1;
39+
},
40+
staleTime: 1000 * 60 * 5,
41+
gcTime: 1000 * 60 * 10,
42+
});
43+
44+
const postLists = data?.pages.flatMap((page) => page?.content) || [];
45+
46+
useEffect(() => {
47+
if (!hasNextPage) return;
48+
if (inView && !isFetchingNextPage) {
49+
fetchNextPage();
50+
}
51+
}, [inView, hasNextPage, isFetchingNextPage, fetchNextPage]);
52+
53+
if (isError) {
54+
navigate('/notFound');
55+
}
1156

1257
return (
1358
<>
14-
<main className={twMerge('flex grow flex-col px-5 pt-20 pb-10', !isMyBoard && 'mt-[-25px]')}>
15-
{isMyBoard ? (
16-
<PageTitle className="mx-auto mb-11">내가 올린 게시물</PageTitle>
59+
<main className="mt-[-25px] flex grow flex-col px-5 pt-20 pb-10">
60+
<>
61+
<NoticeRollingPaper />
62+
<PageTitle className="mx-auto mt-4">게시판</PageTitle>
63+
<p className="text-gray-60 caption-m mt-4.5 text-center">
64+
따숨이에게 힘이 되었던 다양한 편지들을 모아두었어요
65+
</p>
66+
</>
67+
{isLoading ? (
68+
<p>loading</p>
1769
) : (
18-
<>
19-
<NoticeRollingPaper />
20-
<PageTitle className="mx-auto mt-4">게시판</PageTitle>
21-
<p className="text-gray-60 caption-m mt-4.5 text-center">
22-
따숨이에게 힘이 되었던 다양한 편지들을 모아두었어요
23-
</p>
24-
</>
70+
<section className="mt-6 grid grid-cols-2 gap-x-5 gap-y-4">
71+
{postLists.map((item, index) => {
72+
return (
73+
<LetterPreview
74+
key={index}
75+
id={item?.sharePostId || 0}
76+
to={item?.receiverZipCode || 'ERROR'}
77+
from={item?.writerZipCode || 'ERROR'}
78+
content={item?.content || 'no Data'}
79+
ref={index === postLists.length - 1 ? ref : null}
80+
/>
81+
);
82+
})}
83+
</section>
2584
)}
26-
<section className="mt-6 grid grid-cols-2 gap-x-5 gap-y-4">
27-
{Array.from({ length: 10 }).map((_, index) => (
28-
<LetterPreview
29-
key={index}
30-
id={`${index}`}
31-
to="12E21"
32-
from="12E21"
33-
content="저희가 주고 받은 행운의 편지 저희가 주고 받은 행운의 편지 저희가 주고 받은 행운의 편지
34-
저희가 주고 받은 행운의 편지"
35-
/>
36-
))}
37-
</section>
3885
</main>
3986
<BackgroundBottom />
4087
</>

0 commit comments

Comments
 (0)