-
Notifications
You must be signed in to change notification settings - Fork 6
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[FE] Refactor/#642: 북마크 및 전체보기 페이지 API 로직 수정 및 React-Query 적용 #644
Changes from 16 commits
3717ebd
9ab2f36
ab32302
6f12bb4
971a3b6
76b4ae7
5ca854e
89c5256
7dd724a
6038f39
0efd169
fb7234b
10e9713
5c97955
e9a8463
a53b435
8db43f2
4b3e0f9
f2fca6e
645fbf2
1e1a7a3
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
import { useQuery } from '@tanstack/react-query'; | ||
|
||
import { getAllTopics } from '../../apis/new'; | ||
|
||
const useGetAllTopics = () => { | ||
const { isLoading, data: allTopics } = useQuery({ | ||
queryKey: ['GetAllTopics'], | ||
queryFn: getAllTopics, | ||
}); | ||
|
||
return { isLoading, allTopics }; | ||
}; | ||
|
||
export default useGetAllTopics; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
import { useQuery } from '@tanstack/react-query'; | ||
|
||
import { getBestTopics } from '../../apis/new'; | ||
|
||
const useGetBestTopics = () => { | ||
const { isLoading, data: bestTopics } = useQuery({ | ||
queryKey: ['GetBestTopics'], | ||
queryFn: getBestTopics, | ||
}); | ||
|
||
return { isLoading, bestTopics }; | ||
}; | ||
|
||
export default useGetBestTopics; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
import { useQuery } from '@tanstack/react-query'; | ||
|
||
import { getBookmarks } from '../../apis/new'; | ||
|
||
const useGetBookmarks = () => { | ||
const { isLoading, data: bookmarks } = useQuery({ | ||
queryKey: ['GetBookmarks'], | ||
queryFn: getBookmarks, | ||
}); | ||
|
||
return { isLoading, bookmarks }; | ||
}; | ||
|
||
export default useGetBookmarks; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
import { useQuery } from '@tanstack/react-query'; | ||
|
||
import { getNewestTopics } from '../../apis/new'; | ||
|
||
const useGetNewestTopics = () => { | ||
const { isLoading, data: newestTopics } = useQuery({ | ||
queryKey: ['GetNewestTopics'], | ||
queryFn: getNewestTopics, | ||
}); | ||
|
||
return { isLoading, newestTopics }; | ||
}; | ||
|
||
export default useGetNewestTopics; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,98 @@ | ||
import axios, { | ||
AxiosInstance, | ||
AxiosRequestConfig, | ||
AxiosRequestHeaders, | ||
} from 'axios'; | ||
|
||
const API_POSTFIX = 'api'; | ||
const BASE_URL = process.env.APP_URL || `https://mapbefine.com/${API_POSTFIX}`; | ||
const axiosInstance = axios.create({ | ||
baseURL: BASE_URL, | ||
headers: { Authorization: `Bearer ${localStorage.getItem('userToken')}` }, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
헤더에 저렇게 안해주면 제가 맡았던 home페이지에서 "Bearer "이게 Authorization에 무조건 들어가서 에러나는것 같은데 잘 동작하나요? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 예 어제 오류나서 바꿔주었습니다 ㅋㅋㅋㅋ |
||
}); | ||
|
||
let refreshResponse: Promise<Response> | null = null; | ||
|
||
export interface HttpClient extends AxiosInstance { | ||
get<T = unknown>(url: string, config?: AxiosRequestConfig): Promise<T>; | ||
post<T = unknown>( | ||
url: string, | ||
data?: any, | ||
config?: AxiosRequestConfig, | ||
): Promise<T>; | ||
patch<T = unknown>( | ||
url: string, | ||
data?: any, | ||
config?: AxiosRequestConfig, | ||
): Promise<T>; | ||
put<T = unknown>( | ||
url: string, | ||
data?: any, | ||
config?: AxiosRequestConfig, | ||
): Promise<T>; | ||
delete<T = unknown>(url: string, config?: AxiosRequestConfig): Promise<T>; | ||
} | ||
|
||
export const http: HttpClient = axiosInstance; | ||
|
||
http.interceptors.response.use((res) => res.data); | ||
http.interceptors.request.use( | ||
async (config) => { | ||
const userToken = localStorage.getItem('userToken'); | ||
|
||
if (userToken && isTokenExpired(userToken)) { | ||
await updateToken(config.headers); | ||
} | ||
return config; | ||
}, | ||
(error) => Promise.reject(error), | ||
); | ||
|
||
const isTokenExpired = (token: string) => { | ||
const decodedPayloadObject = decodeToken(token); | ||
return decodedPayloadObject.exp * 1000 < Date.now(); | ||
}; | ||
|
||
const decodeToken = (token: string) => { | ||
const tokenParts = token.split('.'); | ||
|
||
if (tokenParts.length !== 3) { | ||
throw new Error('토큰이 잘못되었습니다.'); | ||
} | ||
|
||
const decodedPayloadString = atob(tokenParts[1]); | ||
|
||
return JSON.parse(decodedPayloadString); | ||
}; | ||
|
||
async function updateToken(headers: AxiosRequestHeaders) { | ||
const response = await refreshToken(headers); | ||
const responseCloned = response.clone(); | ||
const newToken = await responseCloned.json(); | ||
|
||
localStorage.setItem('userToken', newToken.accessToken); | ||
} | ||
|
||
async function refreshToken(headers: AxiosRequestHeaders): Promise<Response> { | ||
if (refreshResponse !== null) { | ||
return refreshResponse; | ||
} | ||
|
||
const accessToken = localStorage.getItem('userToken'); | ||
refreshResponse = fetch(`${BASE_URL}/refresh-token`, { | ||
method: 'POST', | ||
headers, | ||
body: JSON.stringify({ | ||
accessToken, | ||
}), | ||
}); | ||
|
||
const responseData = await refreshResponse; | ||
refreshResponse = null; | ||
|
||
if (!responseData.ok) { | ||
throw new Error('Failed to refresh access token.'); | ||
} | ||
|
||
return responseData; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
import { TopicCardProps } from '../../types/Topic'; | ||
import { http } from './http'; | ||
|
||
export const getBookmarks = () => | ||
http.get<TopicCardProps[]>('/members/my/bookmarks'); | ||
|
||
export const getNewestTopics = () => | ||
http.get<TopicCardProps[]>('/topics/newest'); | ||
|
||
export const getAllTopics = () => http.get<TopicCardProps[]>('/topics'); | ||
|
||
export const getBestTopics = () => http.get<TopicCardProps[]>('/topics/bests'); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 아하 어차피 각각 분리가 되어있으니 바로 url을 넣어주신 거군요! 너무 좋습니닷 👍 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 네네 각 요청 함수에 맞는 React Query 훅을 작성할 계획이니 url을 파라미터로 받아올 환경이 아닐테고 그에 따라서 url을 직접 지정해주었습니다. |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,53 +1,21 @@ | ||
import { keyframes, styled } from 'styled-components'; | ||
|
||
import Flex from '../common/Flex'; | ||
import Box from '../common/Box'; | ||
import Space from '../common/Space'; | ||
import SkeletonBox from './common/SkeletonBox'; | ||
|
||
function TopicCardSkeleton() { | ||
return ( | ||
<Flex $flexDirection="row"> | ||
<SkeletonImg /> | ||
<Space size={2} /> | ||
<Flex $flexDirection="column"> | ||
<SkeletonTitle /> | ||
<Space size={5} /> | ||
<SkeletonDescription /> | ||
</Flex> | ||
</Flex> | ||
<Box> | ||
<SkeletonBox width="100%" $maxWidth={212} ratio="1.6 / 1" /> | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. styled component로 만들었던 SkeletonImg나 SkeletonTitle을 효율적으로 쓸 수 있도록 SkeletonBox를 만들었군요 아주 멋집니다 👍 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 스켈레톤 한 번 싹 정리해볼 필요가 있겠네용. 예전 디자인이랑 많이 달라져서.. |
||
<Space size={1} /> | ||
<SkeletonBox width={212} height={25} /> | ||
<Space size={5} /> | ||
<SkeletonBox width={100} height={25} /> | ||
<Space size={1} /> | ||
<SkeletonBox width={212} height={46} /> | ||
</Box> | ||
); | ||
} | ||
|
||
const skeletonAnimation = keyframes` | ||
from { | ||
opacity: 0.1; | ||
} | ||
to { | ||
opacity: 1; | ||
} | ||
`; | ||
|
||
const SkeletonImg = styled.div` | ||
width: 138px; | ||
height: 138px; | ||
|
||
border-radius: 8px; | ||
|
||
background: ${({ theme }) => theme.color.lightGray}; | ||
animation: ${skeletonAnimation} 1s infinite; | ||
`; | ||
|
||
const SkeletonTitle = styled.div` | ||
width: 172px; | ||
height: 32px; | ||
|
||
border-radius: 8px; | ||
|
||
background: ${({ theme }) => theme.color.lightGray}; | ||
animation: ${skeletonAnimation} 1s infinite; | ||
`; | ||
|
||
const SkeletonDescription = styled(SkeletonTitle)` | ||
height: 80px; | ||
`; | ||
|
||
export default TopicCardSkeleton; |
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 스켈레톤 변경 좋습네다~! 😄 |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,26 +1,35 @@ | ||
import { styled } from 'styled-components'; | ||
|
||
import Space from '../common/Space'; | ||
import TopicCardSkeleton from './TopicCardSkeleton'; | ||
|
||
function TopicCardContainerSkeleton() { | ||
function TopicListSkeleton() { | ||
return ( | ||
<Wrapper> | ||
<TopicCardSkeleton /> | ||
<TopicCardSkeleton /> | ||
<TopicCardSkeleton /> | ||
<TopicCardSkeleton /> | ||
<TopicCardSkeleton /> | ||
<TopicCardSkeleton /> | ||
</Wrapper> | ||
<> | ||
<TopicCardWrapper> | ||
<TopicCardSkeleton /> | ||
<TopicCardSkeleton /> | ||
<TopicCardSkeleton /> | ||
<TopicCardSkeleton /> | ||
<TopicCardSkeleton /> | ||
</TopicCardWrapper> | ||
<Space size={4} /> | ||
<TopicCardWrapper> | ||
<TopicCardSkeleton /> | ||
<TopicCardSkeleton /> | ||
<TopicCardSkeleton /> | ||
<TopicCardSkeleton /> | ||
<TopicCardSkeleton /> | ||
</TopicCardWrapper> | ||
</> | ||
); | ||
} | ||
|
||
const Wrapper = styled.section` | ||
const TopicCardWrapper = styled.section` | ||
display: flex; | ||
flex-wrap: wrap; | ||
gap: 20px; | ||
width: 1036px; | ||
height: 300px; | ||
width: 1140px; | ||
`; | ||
|
||
export default TopicCardContainerSkeleton; | ||
export default TopicListSkeleton; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,38 @@ | ||
import styled, { keyframes } from 'styled-components'; | ||
|
||
import { convertCSS } from '../../../utils/convertCSS'; | ||
|
||
interface Props { | ||
width?: number | string; | ||
height?: number | string; | ||
$maxWidth?: number | string; | ||
$maxHeight?: number | string; | ||
ratio?: string; | ||
radius?: number | string; | ||
} | ||
|
||
const skeletonAnimation = keyframes` | ||
from { | ||
opacity: 1; | ||
} | ||
50% { | ||
opacity: 0.6; | ||
} | ||
to { | ||
opacity: 1; | ||
} | ||
`; | ||
|
||
const SkeletonBox = styled.div<Props>` | ||
width: ${({ width }) => width && convertCSS(width)}; | ||
height: ${({ height }) => height && convertCSS(height)}; | ||
max-width: ${({ $maxWidth }) => $maxWidth && convertCSS($maxWidth)}; | ||
max-height: ${({ $maxHeight }) => $maxHeight && convertCSS($maxHeight)}; | ||
aspect-ratio: ${({ ratio }) => ratio}; | ||
border-radius: ${({ radius, theme }) => | ||
(radius && convertCSS(radius)) || theme.radius.small}; | ||
background: ${({ theme }) => theme.color.lightGray}; | ||
animation: ${skeletonAnimation} 1s infinite; | ||
`; | ||
|
||
export default SkeletonBox; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
useQuery를 useSuspenseQuery로 바꾸면 SeeAllTopics.tsx에서 isloading 상태를 신경안쓰고 Suspense fallback으로 관리하면 될꺼같은데 카톡에서 말한 내용이 이부분인가요?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
뎃츠롸잇~ 제가 푸쉬를 안했군요