From 7a27cc9d4bb983a5299ab3fc91034ac3d98ba55b Mon Sep 17 00:00:00 2001 From: saengmotmi Date: Tue, 16 Jul 2024 22:49:08 +0900 Subject: [PATCH 01/14] =?UTF-8?q?fix:=20axios=20instance=20=EA=B5=90?= =?UTF-8?q?=EC=B2=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/app/(hasGNB)/feed/me/page.tsx | 18 ++---- apps/next-app/src/entities/item/api/index.ts | 8 +-- apps/next-app/src/features/auth/token.ts | 15 +---- .../auth/withAuthQueryServerSideProps.ts | 19 ++---- apps/next-app/src/pages/oauth/index.tsx | 3 - apps/next-app/src/services/account/index.ts | 7 ++- apps/next-app/src/services/api/feedoongApi.ts | 15 ----- apps/next-app/src/services/api/index.ts | 15 ++--- apps/next-app/src/services/auth/index.ts | 28 +++++---- apps/next-app/src/services/feeds/index.ts | 62 +++++++++++-------- .../src/services/recommendations/index.ts | 24 +++---- .../src/services/subscriptions/index.ts | 13 ++-- .../src/services/types/_generated/channel.ts | 3 +- .../src/services/types/_generated/item.ts | 3 +- .../src/services/types/_generated/like.ts | 3 +- .../types/_generated/status-controller.ts | 3 +- .../services/types/_generated/subscription.ts | 3 +- .../src/services/types/_generated/user.ts | 3 +- apps/next-app/src/utils/auth.ts | 5 +- 19 files changed, 102 insertions(+), 148 deletions(-) delete mode 100644 apps/next-app/src/services/api/feedoongApi.ts diff --git a/apps/next-app/src/app/(hasGNB)/feed/me/page.tsx b/apps/next-app/src/app/(hasGNB)/feed/me/page.tsx index 66cb5b03..dd5fc9ff 100644 --- a/apps/next-app/src/app/(hasGNB)/feed/me/page.tsx +++ b/apps/next-app/src/app/(hasGNB)/feed/me/page.tsx @@ -3,23 +3,15 @@ import type { NextPage } from 'next' import { cookies } from 'next/headers' import { Suspense } from 'react' -import { itemQueries } from 'entities/item/api' -import MyFeed from 'views/myFeed' +import { SkeletonPostType } from 'components/common/Skeleton' import { getQueryClient } from 'core/getQueryClient' -import { feedoongApi } from 'services/api' -import { setAuthorizationHeader } from 'features/auth/token' +import { itemQueries } from 'entities/item/api' import { checkLoggedIn } from 'shared/utils/checkLoggedIn' -import { SkeletonPostType } from 'components/common/Skeleton' +import MyFeed from 'views/myFeed' const FeedMePage: NextPage = () => { - const api = feedoongApi() - const cookieStore = cookies() - const accessToken = `${cookieStore.get('accessToken')?.value}` - - setAuthorizationHeader(api, accessToken, { type: 'Bearer' }) - const queryClient = getQueryClient() - void queryClient.prefetchInfiniteQuery(itemQueries.list(api)) + void queryClient.prefetchInfiniteQuery(itemQueries.list()) return ( @@ -28,7 +20,7 @@ const FeedMePage: NextPage = () => { ))} > - + ) diff --git a/apps/next-app/src/entities/item/api/index.ts b/apps/next-app/src/entities/item/api/index.ts index 132ccdd5..bb58292d 100644 --- a/apps/next-app/src/entities/item/api/index.ts +++ b/apps/next-app/src/entities/item/api/index.ts @@ -1,18 +1,14 @@ import { infiniteQueryOptions } from '@tanstack/react-query' -import type { AxiosInstance } from 'axios' -import { getFeeds, getFeedsServerSide } from 'services/feeds' +import { getFeeds } from 'services/feeds' // import { getItemsUsingGET } from 'services/types/_generated/item' export const itemQueries = { all: () => ['item'], - list: (api?: AxiosInstance) => + list: () => infiniteQueryOptions({ queryKey: [...itemQueries.all(), 'list'], queryFn: ({ pageParam }) => { - if (api) { - return getFeedsServerSide(api)(pageParam) - } return getFeeds(pageParam) }, initialPageParam: 1, diff --git a/apps/next-app/src/features/auth/token.ts b/apps/next-app/src/features/auth/token.ts index 806c7178..9f2e6f36 100644 --- a/apps/next-app/src/features/auth/token.ts +++ b/apps/next-app/src/features/auth/token.ts @@ -1,6 +1,5 @@ -import Cookies from 'js-cookie' -import type { AxiosInstance } from 'axios' import dayjs from 'dayjs' +import Cookies from 'js-cookie' import { AccessToken, RefreshToken } from 'constants/auth' @@ -26,15 +25,3 @@ export const setAccessTokenToCookie = (token: string) => { sameSite: 'lax', }) } - -export const setAuthorizationHeader = ( - api: AxiosInstance, - token: string, - options?: { - type: 'Bearer' | 'Basic' - } -) => { - api.defaults.headers.common['Authorization'] = options?.type - ? `${options.type} ${token}` - : token -} diff --git a/apps/next-app/src/features/auth/withAuthQueryServerSideProps.ts b/apps/next-app/src/features/auth/withAuthQueryServerSideProps.ts index 3ad94cfa..4edaca58 100644 --- a/apps/next-app/src/features/auth/withAuthQueryServerSideProps.ts +++ b/apps/next-app/src/features/auth/withAuthQueryServerSideProps.ts @@ -1,13 +1,10 @@ -import type { GetServerSideProps, GetServerSidePropsContext } from 'next' import { dehydrate, QueryClient } from '@tanstack/react-query' -import { parseCookies } from 'nookies' +import type { GetServerSideProps, GetServerSidePropsContext } from 'next' +import type { feedoongApi } from 'services/api' import type { UserProfile } from 'services/auth' -import { getUserInfoServerSide } from 'services/auth' +import { getUserInfo } from 'services/auth' import { CACHE_KEYS } from 'services/cacheKeys' -import { setAuthorizationHeader } from 'features/auth/token' -import { feedoongApi } from 'services/api' -import { AccessToken } from 'constants/auth' export type GetServerSidePropsContextWithAuthClient = GetServerSidePropsContext & { @@ -20,20 +17,12 @@ export const withAuthQueryServerSideProps = ( ) => { return async (context: GetServerSidePropsContextWithAuthClient) => { try { - const api = feedoongApi() - context.api = api - const cookies = parseCookies( - context as (typeof parseCookies)['arguments'] - ) - - setAuthorizationHeader(api, cookies[AccessToken], { type: 'Bearer' }) - const queryClient = new QueryClient() context.queryClient = queryClient await queryClient.prefetchQuery({ queryKey: CACHE_KEYS.me, - queryFn: getUserInfoServerSide(api), + queryFn: getUserInfo, }) if (!getServerSidePropsFunc) { diff --git a/apps/next-app/src/pages/oauth/index.tsx b/apps/next-app/src/pages/oauth/index.tsx index f7f8e2a4..4ae3e2fd 100644 --- a/apps/next-app/src/pages/oauth/index.tsx +++ b/apps/next-app/src/pages/oauth/index.tsx @@ -5,11 +5,9 @@ import humps from 'humps' import { useEffect } from 'react' import { submitAccessToken } from 'services/auth' -import api from 'services/api' import { CACHE_KEYS } from 'services/cacheKeys' import { setAccessTokenToCookie, - setAuthorizationHeader, setRefreshTokenToCookie, } from 'features/auth/token' @@ -26,7 +24,6 @@ const Oauth = () => { if (data) { setRefreshTokenToCookie(data.refreshToken) setAccessTokenToCookie(data.accessToken) - setAuthorizationHeader(api, data.accessToken, { type: 'Bearer' }) client.setQueryData(CACHE_KEYS.me, data) router.replace('/') } diff --git a/apps/next-app/src/services/account/index.ts b/apps/next-app/src/services/account/index.ts index 401f9466..e7a886f1 100644 --- a/apps/next-app/src/services/account/index.ts +++ b/apps/next-app/src/services/account/index.ts @@ -1,5 +1,8 @@ -import api from 'services/api' +import { feedoongApi } from 'services/api' export const deleteAccount = () => { - return api.delete('/users') + return feedoongApi({ + method: 'DELETE', + url: '/users', + }) } diff --git a/apps/next-app/src/services/api/feedoongApi.ts b/apps/next-app/src/services/api/feedoongApi.ts deleted file mode 100644 index 8dc28df2..00000000 --- a/apps/next-app/src/services/api/feedoongApi.ts +++ /dev/null @@ -1,15 +0,0 @@ -import type { AxiosHeaders } from 'axios' - -import api from '.' - -type FeedoongApiArg = { - url: string - method: 'get' | 'post' | 'delete' | 'put' - params?: unknown - headers?: Partial - data?: unknown -} - -export const feedoongApi = ({ url, method, params }: FeedoongApiArg) => { - return api[method](url, params) -} diff --git a/apps/next-app/src/services/api/index.ts b/apps/next-app/src/services/api/index.ts index ecfe3c79..03d383e8 100644 --- a/apps/next-app/src/services/api/index.ts +++ b/apps/next-app/src/services/api/index.ts @@ -1,4 +1,4 @@ -import type { AxiosResponse } from 'axios' +import type { AxiosRequestConfig, AxiosResponse } from 'axios' import Axios, { AxiosError } from 'axios' import humps from 'humps' import httpStatus from 'http-status-codes' @@ -12,7 +12,7 @@ import { const { camelizeKeys } = humps -export const feedoongApi = () => { +export const feedoongApi = (config: AxiosRequestConfig): Promise => { const accessToken = getAccessTokenFromCookie() const _api = Axios.create({ @@ -27,6 +27,11 @@ export const feedoongApi = () => { _api.interceptors.response.use( // try (response) => { + config.headers = { + ...config.headers, + Authorization: `Bearer ${accessToken}`, + } + return Promise.resolve( camelizeKeys(response.data) ) as unknown as AxiosResponse @@ -54,9 +59,5 @@ export const feedoongApi = () => { return config }) - return _api + return _api(config) } - -const api = feedoongApi() - -export default api diff --git a/apps/next-app/src/services/auth/index.ts b/apps/next-app/src/services/auth/index.ts index e078ccd5..d0b1a3fe 100644 --- a/apps/next-app/src/services/auth/index.ts +++ b/apps/next-app/src/services/auth/index.ts @@ -6,13 +6,14 @@ import axios, { } from 'axios' import { getApiEndpoint } from 'envs' -import api from 'services/api' +import { feedoongApi } from 'services/api' import { getRefreshTokenFromCookie, setAccessTokenToCookie, - setAuthorizationHeader, + // setAuthorizationHeader, setRefreshTokenToCookie, } from 'features/auth/token' +import type UserProfile from 'pages/[userName]' export interface UserProfile { email: string @@ -27,23 +28,26 @@ export interface SignUpResponse extends UserProfile { } export const submitAccessToken = (token: string) => { - return api.post(`/users/login/google`, null, { + return feedoongApi({ + method: 'POST', + url: '/users/login/google', + data: null, params: { accessToken: token }, }) } export const getUserInfo = () => { - return api.get(`/users/me`) + return feedoongApi({ + method: 'GET', + url: '/users/me', + }) } export const getUserInfoByUsername = (username: string) => { - return api.get>( - `/users/${username}/info` - ) -} - -export const getUserInfoServerSide = (_api: AxiosInstance) => () => { - return _api.get(`/users/me`) + return feedoongApi>({ + method: 'GET', + url: `/users/${username}/info`, + }) } export const refreshAccessToken = async ( @@ -66,8 +70,6 @@ export const refreshAccessToken = async ( setRefreshTokenToCookie(newRefreshToken) setAccessTokenToCookie(newAccessToken) - setAuthorizationHeader(_api, newAccessToken, { type: 'Bearer' }) - // 필요한 코드인지 확인 필요 if (!originalRequest?.headers) { originalRequest!.headers = {} as AxiosRequestHeaders diff --git a/apps/next-app/src/services/feeds/index.ts b/apps/next-app/src/services/feeds/index.ts index 76e1b7cd..047b86c1 100644 --- a/apps/next-app/src/services/feeds/index.ts +++ b/apps/next-app/src/services/feeds/index.ts @@ -1,6 +1,4 @@ -import type { AxiosInstance } from 'axios' - -import api from 'services/api' +import { feedoongApi } from 'services/api/index' import type { Feed, LikePostResponse, @@ -11,7 +9,9 @@ import type { } from 'types/feeds' export const getFeeds = (page = 1, size = 10) => { - return api.get(`/items`, { + return feedoongApi({ + method: 'GET', + url: '/items', params: { page, size, @@ -19,19 +19,10 @@ export const getFeeds = (page = 1, size = 10) => { }) } -export const getFeedsServerSide = - (_api: AxiosInstance) => - (page = 1, size = 10) => { - return _api.get(`/items`, { - params: { - page, - size, - }, - }) - } - export const getChannel = (channelId: string, page = 1, size = 10) => { - return api.get(`/items/channel/${channelId}`, { + return feedoongApi({ + method: 'GET', + url: `/items/channel/${channelId}`, params: { page, size, @@ -40,7 +31,9 @@ export const getChannel = (channelId: string, page = 1, size = 10) => { } export const checkUrlAsRss = (url: string) => { - return api.get(`/channels/preview`, { + return feedoongApi({ + method: 'GET', + url: `/channels/preview`, params: { url }, }) } @@ -52,7 +45,9 @@ export const checkUrlAsDirectRss = ({ homeUrl: string rssFeedUrl: string }) => { - return api.get(`/channels/preview/rss`, { + return feedoongApi({ + method: 'GET', + url: `/channels/preview/rss`, params: { homeUrl, rssFeedUrl }, }) } @@ -61,31 +56,48 @@ export const submitRssUrl = (params: Partial) => { if (!params.url || !params.feedUrl) { throw new Error('url and feedUrl are required') } - return api.post(`/channels`, { - ...params, + return feedoongApi({ + method: 'POST', + url: `/channels`, + data: { + ...params, + }, }) } export const likePost = (id: string) => { - return api.post(`/likes/${id}`) + return feedoongApi({ + method: 'POST', + url: `/likes/${id}`, + }) } export const unlikePost = (id: string) => { - return api.delete(`/likes/${id}`) + return feedoongApi({ + method: 'DELETE', + url: `/likes/${id}`, + }) } export const getLikedPosts = (page: number) => { - return api.get(`/items/liked`, { + return feedoongApi({ + method: 'GET', + url: `/items/liked`, params: { page }, }) } export const submitViewedPost = (id: number) => { - return api.post(`/items/view/${id}`) + return feedoongApi({ + method: 'POST', + url: `/items/view/${id}`, + }) } export const getLikedPostsByUsername = (page: number, username?: string) => { - return api.get(`/users/${username}/liked-items`, { + return feedoongApi({ + method: 'GET', + url: `/users/${username}/liked-items`, params: { page }, }) } diff --git a/apps/next-app/src/services/recommendations/index.ts b/apps/next-app/src/services/recommendations/index.ts index 07758ee8..2d0d0cea 100644 --- a/apps/next-app/src/services/recommendations/index.ts +++ b/apps/next-app/src/services/recommendations/index.ts @@ -1,23 +1,17 @@ -import type { AxiosInstance } from 'axios' - -import api from 'services/api' +import { feedoongApi } from 'services/api' import type { Post } from 'types/feeds' import type { Channel } from 'types/subscriptions' export const getRecommendedChannels = () => { - return api.get(`/channels/recommended`) + return feedoongApi<{ channels: Channel[] }>({ + method: 'GET', + url: `/channels/recommended`, + }) } export const getRecommendedPosts = () => { - return api.get(`/items/recommended`) -} - -/** @note 새로운 api 이므로 연결 필요 */ -export const getRecommendedChannelsServerSide = (_api: AxiosInstance) => () => { - return _api.get(`/channels/recommended`) -} - -/** @note 새로운 api 이므로 연결 필요 */ -export const getRecommendedPostsServerSide = (_api: AxiosInstance) => () => { - return _api.get(`/items/recommended`) + return feedoongApi<{ items: Post[] }>({ + method: 'GET', + url: `/items/recommended`, + }) } diff --git a/apps/next-app/src/services/subscriptions/index.ts b/apps/next-app/src/services/subscriptions/index.ts index 8f2aeda7..30fd0ab4 100644 --- a/apps/next-app/src/services/subscriptions/index.ts +++ b/apps/next-app/src/services/subscriptions/index.ts @@ -1,18 +1,23 @@ -import api from 'services/api' +import { feedoongApi } from 'services/api' import type { Channels } from 'types/subscriptions' export const getChannels = (page: number) => { - return api.get(`/subscriptions`, { + return feedoongApi({ + url: `/subscriptions`, params: { page }, }) } export const deleteChannel = (channelId: number) => { - return api.delete(`/subscriptions/${channelId}`) + return feedoongApi({ + url: `/subscriptions/${channelId}`, + method: 'DELETE', + }) } export const getChannelsByUsername = (page: number, username?: string) => { - return api.get(`/users/${username}/subscriptions`, { + return feedoongApi({ + url: `/users/${username}/subscriptions`, params: { page }, }) } diff --git a/apps/next-app/src/services/types/_generated/channel.ts b/apps/next-app/src/services/types/_generated/channel.ts index dfa8fa14..9f7bda46 100644 --- a/apps/next-app/src/services/types/_generated/channel.ts +++ b/apps/next-app/src/services/types/_generated/channel.ts @@ -13,8 +13,7 @@ import type { GetChannelPreviewViaRssFeedUsingGETParams, RecommendedChannelListResponse } from './apiDocumentation.schemas' -// import { feedoongApi } from '../../api/index'; -import { feedoongApi } from '../../api/feedoongApi' +import { feedoongApi } from '../../api/index'; diff --git a/apps/next-app/src/services/types/_generated/item.ts b/apps/next-app/src/services/types/_generated/item.ts index dd21f6f9..62764251 100644 --- a/apps/next-app/src/services/types/_generated/item.ts +++ b/apps/next-app/src/services/types/_generated/item.ts @@ -14,8 +14,7 @@ import type { RecommendedItemListResponse, UserItemListResponse } from './apiDocumentation.schemas' -// import { feedoongApi } from '../../api/index'; -import { feedoongApi } from '../../api/feedoongApi' +import { feedoongApi } from '../../api/index'; diff --git a/apps/next-app/src/services/types/_generated/like.ts b/apps/next-app/src/services/types/_generated/like.ts index bc028492..83e0afa7 100644 --- a/apps/next-app/src/services/types/_generated/like.ts +++ b/apps/next-app/src/services/types/_generated/like.ts @@ -8,8 +8,7 @@ import type { LikeResponse } from './apiDocumentation.schemas' -// import { feedoongApi } from '../../api/index'; -import { feedoongApi } from '../../api/feedoongApi' +import { feedoongApi } from '../../api/index'; diff --git a/apps/next-app/src/services/types/_generated/status-controller.ts b/apps/next-app/src/services/types/_generated/status-controller.ts index 3b70e378..58648f92 100644 --- a/apps/next-app/src/services/types/_generated/status-controller.ts +++ b/apps/next-app/src/services/types/_generated/status-controller.ts @@ -8,8 +8,7 @@ import type { CheckHealthUsingGET200 } from './apiDocumentation.schemas' -// import { feedoongApi } from '../../api/index'; -import { feedoongApi } from '../../api/feedoongApi' +import { feedoongApi } from '../../api/index'; diff --git a/apps/next-app/src/services/types/_generated/subscription.ts b/apps/next-app/src/services/types/_generated/subscription.ts index 7ec7b127..bd604d9d 100644 --- a/apps/next-app/src/services/types/_generated/subscription.ts +++ b/apps/next-app/src/services/types/_generated/subscription.ts @@ -9,8 +9,7 @@ import type { GetSubscriptionsUsingGETParams, SubscriptionListResponse } from './apiDocumentation.schemas' -// import { feedoongApi } from '../../api/index'; -import { feedoongApi } from '../../api/feedoongApi' +import { feedoongApi } from '../../api/index'; diff --git a/apps/next-app/src/services/types/_generated/user.ts b/apps/next-app/src/services/types/_generated/user.ts index 3253994f..7e7d90f3 100644 --- a/apps/next-app/src/services/types/_generated/user.ts +++ b/apps/next-app/src/services/types/_generated/user.ts @@ -17,8 +17,7 @@ import type { UserItemListResponse, UserSubscriptionListResponse } from './apiDocumentation.schemas' -// import { feedoongApi } from '../../api/index'; -import { feedoongApi } from '../../api/feedoongApi' +import { feedoongApi } from '../../api/index'; diff --git a/apps/next-app/src/utils/auth.ts b/apps/next-app/src/utils/auth.ts index ab74bb6d..bd565ea7 100644 --- a/apps/next-app/src/utils/auth.ts +++ b/apps/next-app/src/utils/auth.ts @@ -1,9 +1,7 @@ -import type { NextRequest } from 'next/server' import Cookies from 'js-cookie' +import type { NextRequest } from 'next/server' import { AccessToken, RefreshToken } from 'constants/auth' -import api from 'services/api' -import { setAuthorizationHeader } from 'features/auth/token' export const isLoginValidServerSide = (request: NextRequest) => { const accessToken = request.cookies.get(AccessToken) @@ -29,6 +27,5 @@ export const destroyTokensClientSide = () => { Cookies.remove(RefreshToken) Cookies.remove(AccessToken) - setAuthorizationHeader(api, '') // TODO: Invalidate the tokens on the server } From 8b0fda60411602b6d1a944462369e445746480ae Mon Sep 17 00:00:00 2001 From: saengmotmi Date: Tue, 16 Jul 2024 23:11:46 +0900 Subject: [PATCH 02/14] =?UTF-8?q?fix:=20=ED=83=80=EC=9E=85=20=EC=97=90?= =?UTF-8?q?=EB=9F=AC=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/common/FeedItem/Post/index.tsx | 2 +- .../common/FeedItem/hooks/useReadPost.ts | 4 +- .../common/FeedItem/hooks/useToggleLike.ts | 7 +- .../views/Channel/ChannelDetailContainer.tsx | 18 +-- .../components/views/Feeds/MyFeed/MyFeed.tsx | 12 +- .../views/MyAccount/MyAccountContainer.tsx | 18 +-- .../RssInput/hooks/useRssDirectInputModal.tsx | 16 ++- .../views/RssInput/hooks/useRssInput.tsx | 9 +- .../PostList/hooks/usePostListByUsername.ts | 13 ++- apps/next-app/src/entities/item/api/index.ts | 8 +- apps/next-app/src/envs/index.ts | 2 +- apps/next-app/src/features/channel/index.ts | 6 +- .../post/ui/PostFeedItem/PostFeedItem.tsx | 2 +- apps/next-app/src/services/account/index.ts | 8 -- apps/next-app/src/services/auth/index.ts | 1 - apps/next-app/src/services/feeds/index.ts | 103 ------------------ 16 files changed, 72 insertions(+), 157 deletions(-) delete mode 100644 apps/next-app/src/services/account/index.ts delete mode 100644 apps/next-app/src/services/feeds/index.ts diff --git a/apps/next-app/src/components/common/FeedItem/Post/index.tsx b/apps/next-app/src/components/common/FeedItem/Post/index.tsx index dce957ed..ef3dae8f 100644 --- a/apps/next-app/src/components/common/FeedItem/Post/index.tsx +++ b/apps/next-app/src/components/common/FeedItem/Post/index.tsx @@ -80,7 +80,7 @@ export const PrivatePostType = ({ item }: { item: PrivatePost }) => { src={item.isLiked ? Icons.Bookmark : Icons.BookmarkDeactive} width={16} height={16} - onClick={() => handleLike(String(item.id))} + onClick={() => handleLike(item.id)} priority /> diff --git a/apps/next-app/src/components/common/FeedItem/hooks/useReadPost.ts b/apps/next-app/src/components/common/FeedItem/hooks/useReadPost.ts index db6404d0..6495719b 100644 --- a/apps/next-app/src/components/common/FeedItem/hooks/useReadPost.ts +++ b/apps/next-app/src/components/common/FeedItem/hooks/useReadPost.ts @@ -2,7 +2,7 @@ import { useMutation, useQueryClient } from '@tanstack/react-query' import produce from 'immer' import { CACHE_KEYS } from 'services/cacheKeys' -import { submitViewedPost } from 'services/feeds' +import { viewItemUsingPOST } from 'services/types/_generated/item' import type { Feed, SubmitViewedPost } from 'types/feeds' import { mergeObjectsByMutate } from 'utils/common' @@ -17,7 +17,7 @@ const useReadPost = (item: { id: number }) => { const { mutate: handleRead } = useMutation({ mutationKey: CACHE_KEYS.viewItem(item.id), - mutationFn: submitViewedPost, + mutationFn: viewItemUsingPOST, onSuccess: (data, variables) => { client.setQueryData(CACHE_KEYS.feeds, (prev) => { if (!prev) { diff --git a/apps/next-app/src/components/common/FeedItem/hooks/useToggleLike.ts b/apps/next-app/src/components/common/FeedItem/hooks/useToggleLike.ts index 7a36716e..0321537f 100644 --- a/apps/next-app/src/components/common/FeedItem/hooks/useToggleLike.ts +++ b/apps/next-app/src/components/common/FeedItem/hooks/useToggleLike.ts @@ -3,7 +3,10 @@ import { useMutation, useQueryClient } from '@tanstack/react-query' import Toast from 'components/common/Toast' import { itemQueries } from 'entities/item/api' import { CACHE_KEYS } from 'services/cacheKeys' -import { likePost, unlikePost } from 'services/feeds' +import { + likeUsingPOST, + unlikeUsingDELETE, +} from 'services/types/_generated/like' // TODO: 추후에 useToggleLike 자체를 재작성해야 함. 임시로 인자 타입 변경 const useToggleLike = (item: { id: number; isLiked: boolean }) => { @@ -11,7 +14,7 @@ const useToggleLike = (item: { id: number; isLiked: boolean }) => { const { mutate: handleLike } = useMutation({ mutationKey: CACHE_KEYS.likePost(item.id), - mutationFn: !item.isLiked ? likePost : unlikePost, + mutationFn: !item.isLiked ? likeUsingPOST : unlikeUsingDELETE, onSuccess: async (data) => { client.invalidateQueries(itemQueries.list()) client.invalidateQueries({ queryKey: CACHE_KEYS.feeds }) diff --git a/apps/next-app/src/components/views/Channel/ChannelDetailContainer.tsx b/apps/next-app/src/components/views/Channel/ChannelDetailContainer.tsx index 96cdad78..d032b44b 100644 --- a/apps/next-app/src/components/views/Channel/ChannelDetailContainer.tsx +++ b/apps/next-app/src/components/views/Channel/ChannelDetailContainer.tsx @@ -1,20 +1,20 @@ -import { useState } from 'react' import { useQuery } from '@tanstack/react-query' import { useRouter } from 'next/router' +import { useState } from 'react' import Skeleton from 'react-loading-skeleton' // import * as S from 'components/views/MyPost/PostContainer.style' -import Flex from 'components/common/Flex' import FeedItem from 'components/common/FeedItem/FeedItem' -import { getChannel } from 'services/feeds' -import { CACHE_KEYS } from 'services/cacheKeys' +import Flex from 'components/common/Flex' +import LogoIcon from 'components/common/LogoIcon' +import PageContainer from 'components/common/PageContainer' import Paging from 'components/common/Paging' import { SkeletonPostType } from 'components/common/Skeleton' import { ITEMS_PER_PAGE } from 'constants/pagination' +import { CACHE_KEYS } from 'services/cacheKeys' +import { getItemsOfSubscribedChannelUsingGET } from 'services/types/_generated/item' import { getWellKnownChannelImg } from 'utils' -import PageContainer from 'components/common/PageContainer' -import LogoIcon from 'components/common/LogoIcon' import ChannelSubscription from './ChannalSubscription' import * as S from './ChannelDetailContainer.style' @@ -26,7 +26,11 @@ function PostContainer() { const [currentPage, setCurrentPage] = useState(1) const { data, isLoading } = useQuery({ queryKey: [CACHE_KEYS.likedPosts, { page: currentPage, channel: id }], - queryFn: () => getChannel(id, currentPage), + queryFn: () => + getItemsOfSubscribedChannelUsingGET(Number(id), { + page: currentPage, + size: 10, + }), enabled: !!id, }) diff --git a/apps/next-app/src/components/views/Feeds/MyFeed/MyFeed.tsx b/apps/next-app/src/components/views/Feeds/MyFeed/MyFeed.tsx index e42ab3d9..7a8007ac 100644 --- a/apps/next-app/src/components/views/Feeds/MyFeed/MyFeed.tsx +++ b/apps/next-app/src/components/views/Feeds/MyFeed/MyFeed.tsx @@ -2,18 +2,22 @@ import { useInfiniteQuery } from '@tanstack/react-query' import { useEffect } from 'react' import { useInView } from 'react-intersection-observer' -import { SkeletonPostType } from 'components/common/Skeleton' -import { CACHE_KEYS } from 'services/cacheKeys' -import { getFeeds } from 'services/feeds' import FeedItem from 'components/common/FeedItem' import Loading from 'components/common/Loading' +import { SkeletonPostType } from 'components/common/Skeleton' +import { CACHE_KEYS } from 'services/cacheKeys' +import { getItemsUsingGET } from 'services/types/_generated/item' import * as S from '../FeedsContainer.style' const MyFeed = () => { const { data, fetchNextPage, hasNextPage, isFetchingNextPage, isFetching } = useInfiniteQuery({ queryKey: CACHE_KEYS.feeds, - queryFn: ({ pageParam = 1 }) => getFeeds(pageParam), + queryFn: ({ pageParam = 1 }) => + getItemsUsingGET({ + page: pageParam, + size: 10, + }), initialPageParam: 1, staleTime: 1000 * 60 * 5, getNextPageParam: (lastPage) => diff --git a/apps/next-app/src/components/views/MyAccount/MyAccountContainer.tsx b/apps/next-app/src/components/views/MyAccount/MyAccountContainer.tsx index ffa58d63..0d26a82e 100644 --- a/apps/next-app/src/components/views/MyAccount/MyAccountContainer.tsx +++ b/apps/next-app/src/components/views/MyAccount/MyAccountContainer.tsx @@ -1,18 +1,18 @@ -import React, { useState } from 'react' import { useMutation, useQueryClient } from '@tanstack/react-query' +import { useState } from 'react' +import Button from 'components/common/Button/Button' import Dialog from 'components/common/Dialog' -import Toast from 'components/common/Toast' -import { deleteAccount } from 'services/account' -import { CACHE_KEYS } from 'services/cacheKeys' -import { destroyTokensClientSide } from 'utils/auth' import { copyToClipboard } from 'components/common/FeedItem/FeedItem.utils' +import Input from 'components/common/Input/Input' import PageContainer from 'components/common/PageContainer' +import Toast from 'components/common/Toast' +import { logoutAction } from 'features/auth/logout' import { useGetUserProfile } from 'features/user/userProfile' +import { CACHE_KEYS } from 'services/cacheKeys' +import { deactivateUserUsingDELETE } from 'services/types/_generated/user' +import { destroyTokensClientSide } from 'utils/auth' import { getFeedoongUrl } from '../UserPage/UserPageContainer.utils' -import { logoutAction } from 'features/auth/logout' -import Input from 'components/common/Input/Input' -import Button from 'components/common/Button/Button' import * as S from './MyAccountContainer.style' @@ -23,7 +23,7 @@ const MyAccountContainer = () => { // const nickNameRef = useRef(null) const { mutate: deleteAccountAction } = useMutation({ mutationKey: ['deleteAccount'], - mutationFn: deleteAccount, + mutationFn: deactivateUserUsingDELETE, onSuccess: () => { Toast.show({ content: 'Successfully delete account' }) destroyTokensClientSide() diff --git a/apps/next-app/src/components/views/RssInput/hooks/useRssDirectInputModal.tsx b/apps/next-app/src/components/views/RssInput/hooks/useRssDirectInputModal.tsx index f2c26935..5c6ab336 100644 --- a/apps/next-app/src/components/views/RssInput/hooks/useRssDirectInputModal.tsx +++ b/apps/next-app/src/components/views/RssInput/hooks/useRssDirectInputModal.tsx @@ -6,7 +6,10 @@ import Button from 'components/common/Button' import Flex from 'components/common/Flex' import { ModalLayout, useModal } from 'components/common/Modal' import { CACHE_KEYS } from 'services/cacheKeys' -import { checkUrlAsDirectRss, submitRssUrl } from 'services/feeds' +import { + getChannelPreviewViaRssFeedUsingGET, + registerChannelUsingPOST, +} from 'services/types/_generated/channel' import { getAxiosError, isAxiosError } from 'utils/errors' import BlogUrlInput from '../BlogUrlInput' import { ChannelToast, isRssUrlValid } from '../RssInputContainer.utils' @@ -26,10 +29,11 @@ const useRssDirectInputModal = () => { return } setIsPreviewLoading(true) - const { url: siteUrl, feedUrl } = await checkUrlAsDirectRss({ - homeUrl: rssDirectChannelUrl, - rssFeedUrl: rssDirectRssUrl, - }) + const { url: siteUrl, feedUrl } = + await getChannelPreviewViaRssFeedUsingGET({ + homeUrl: rssDirectChannelUrl, + rssFeedUrl: rssDirectRssUrl, + }) mutateRss({ url: siteUrl, feedUrl }) } catch (error) { if (isAxiosError(error)) { @@ -45,7 +49,7 @@ const useRssDirectInputModal = () => { const { mutate: mutateRss, isPending: isRssSubmitting } = useMutation({ mutationKey: ['/channels'], // 키 값 바꿔야 하나 - mutationFn: submitRssUrl, + mutationFn: registerChannelUsingPOST, onSuccess: () => { setRssDirectChannelUrl('') setRssDirectRssUrl('') diff --git a/apps/next-app/src/components/views/RssInput/hooks/useRssInput.tsx b/apps/next-app/src/components/views/RssInput/hooks/useRssInput.tsx index ee986f20..6c683ed9 100644 --- a/apps/next-app/src/components/views/RssInput/hooks/useRssInput.tsx +++ b/apps/next-app/src/components/views/RssInput/hooks/useRssInput.tsx @@ -3,9 +3,12 @@ import { useState, type ChangeEvent } from 'react' import Notification from 'components/common/Notification' import { CACHE_KEYS } from 'services/cacheKeys' -import { checkUrlAsRss, submitRssUrl } from 'services/feeds' import { getAxiosError, isAxiosError } from 'utils/errors' import { ChannelToast } from '../RssInputContainer.utils' +import { + getChannelPreviewUsingGET, + registerChannelUsingPOST, +} from 'services/types/_generated/channel' const useRssInput = () => { const client = useQueryClient() @@ -16,7 +19,7 @@ const useRssInput = () => { const { mutate, isPending: isSubmitting } = useMutation({ mutationKey: ['/channels'], - mutationFn: submitRssUrl, + mutationFn: registerChannelUsingPOST, onSuccess: () => { setUrl('') client.invalidateQueries({ queryKey: CACHE_KEYS.feeds }) @@ -55,7 +58,7 @@ const useRssInput = () => { e?.preventDefault() setIsPreviewLoading(true) - const { url: siteUrl, feedUrl } = await checkUrlAsRss(url) + const { url: siteUrl, feedUrl } = await getChannelPreviewUsingGET({ url }) mutate({ url: siteUrl, feedUrl }) } catch (error) { diff --git a/apps/next-app/src/components/views/UserPage/List/PostList/hooks/usePostListByUsername.ts b/apps/next-app/src/components/views/UserPage/List/PostList/hooks/usePostListByUsername.ts index a179e71f..21c44095 100644 --- a/apps/next-app/src/components/views/UserPage/List/PostList/hooks/usePostListByUsername.ts +++ b/apps/next-app/src/components/views/UserPage/List/PostList/hooks/usePostListByUsername.ts @@ -3,7 +3,8 @@ import { useRouter } from 'next/router' import { useCheckIsMyProfile } from 'features/user/useCheckIsMyProfile' import { CACHE_KEYS } from 'services/cacheKeys' -import { getLikedPosts, getLikedPostsByUsername } from 'services/feeds' +import { getLikesUsingGET } from 'services/types/_generated/item' +import { getUserLikedItemsUsingGET } from 'services/types/_generated/user' const usePostListByUsername = (username?: string) => { const router = useRouter() @@ -14,8 +15,14 @@ const usePostListByUsername = (username?: string) => { queryKey: [CACHE_KEYS.likedPosts, { page: currentPage }], queryFn: () => isMyProfile - ? getLikedPosts(currentPage) - : getLikedPostsByUsername(currentPage, username), + ? getLikesUsingGET({ + page: currentPage, + size: 10, + }) + : getUserLikedItemsUsingGET(username!, { + page: currentPage, + size: 10, + }), enabled: !!username, }) diff --git a/apps/next-app/src/entities/item/api/index.ts b/apps/next-app/src/entities/item/api/index.ts index bb58292d..e70a97af 100644 --- a/apps/next-app/src/entities/item/api/index.ts +++ b/apps/next-app/src/entities/item/api/index.ts @@ -1,7 +1,6 @@ import { infiniteQueryOptions } from '@tanstack/react-query' -import { getFeeds } from 'services/feeds' -// import { getItemsUsingGET } from 'services/types/_generated/item' +import { getItemsUsingGET } from 'services/types/_generated/item' export const itemQueries = { all: () => ['item'], @@ -9,7 +8,10 @@ export const itemQueries = { infiniteQueryOptions({ queryKey: [...itemQueries.all(), 'list'], queryFn: ({ pageParam }) => { - return getFeeds(pageParam) + return getItemsUsingGET({ + page: pageParam, + size: 10, + }) }, initialPageParam: 1, getNextPageParam: (lastPage) => { diff --git a/apps/next-app/src/envs/index.ts b/apps/next-app/src/envs/index.ts index 8ea15834..78443257 100644 --- a/apps/next-app/src/envs/index.ts +++ b/apps/next-app/src/envs/index.ts @@ -10,7 +10,7 @@ export const getApiEndpoint = () => { case 'staging': case 'development': default: - return 'https://api.feedoong.io/v1' + return 'https://api.feedoong.io' } } diff --git a/apps/next-app/src/features/channel/index.ts b/apps/next-app/src/features/channel/index.ts index d68ab1f3..30cae4c9 100644 --- a/apps/next-app/src/features/channel/index.ts +++ b/apps/next-app/src/features/channel/index.ts @@ -5,8 +5,8 @@ import type { AxiosError } from 'axios' import Toast from 'components/common/Toast' import { ChannelToast } from 'components/views/RssInput/RssInputContainer.utils' import { CACHE_KEYS } from 'services/cacheKeys' -import { submitRssUrl } from 'services/feeds' import { deleteChannel } from 'services/subscriptions' +import { registerChannelUsingPOST } from 'services/types/_generated/channel' import type { Channel } from 'types/subscriptions' import type { ErrorBody } from 'utils/errors' import { getAxiosError } from 'utils/errors' @@ -15,7 +15,7 @@ export const useSubscribeChannel = () => { const client = useQueryClient() return useMutation({ - mutationFn: submitRssUrl, + mutationFn: registerChannelUsingPOST, onSuccess: () => { ChannelToast.addChannel() client.invalidateQueries({ @@ -31,7 +31,7 @@ export const useSubscribeChannel = () => { export const subscribeChannel = async (item: Channel) => { Toast.show({ type: 'promise', - fetchFn: submitRssUrl({ url: item.url, feedUrl: item.feedUrl }), + fetchFn: registerChannelUsingPOST({ url: item.url, feedUrl: item.feedUrl }), content: '새로운 채널이 추가되었어요!', promiseContent: { loading: '채널을 등록중이에요', diff --git a/apps/next-app/src/features/post/ui/PostFeedItem/PostFeedItem.tsx b/apps/next-app/src/features/post/ui/PostFeedItem/PostFeedItem.tsx index 4b05b067..182f0ff5 100644 --- a/apps/next-app/src/features/post/ui/PostFeedItem/PostFeedItem.tsx +++ b/apps/next-app/src/features/post/ui/PostFeedItem/PostFeedItem.tsx @@ -75,7 +75,7 @@ export const PostFeedItem = ({ src={isLiked ? Icons.Bookmark : Icons.BookmarkDeactive} width={16} height={16} - onClick={() => handleLike(String(id))} + onClick={() => handleLike(id)} priority /> )} diff --git a/apps/next-app/src/services/account/index.ts b/apps/next-app/src/services/account/index.ts deleted file mode 100644 index e7a886f1..00000000 --- a/apps/next-app/src/services/account/index.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { feedoongApi } from 'services/api' - -export const deleteAccount = () => { - return feedoongApi({ - method: 'DELETE', - url: '/users', - }) -} diff --git a/apps/next-app/src/services/auth/index.ts b/apps/next-app/src/services/auth/index.ts index d0b1a3fe..806b5ed4 100644 --- a/apps/next-app/src/services/auth/index.ts +++ b/apps/next-app/src/services/auth/index.ts @@ -10,7 +10,6 @@ import { feedoongApi } from 'services/api' import { getRefreshTokenFromCookie, setAccessTokenToCookie, - // setAuthorizationHeader, setRefreshTokenToCookie, } from 'features/auth/token' import type UserProfile from 'pages/[userName]' diff --git a/apps/next-app/src/services/feeds/index.ts b/apps/next-app/src/services/feeds/index.ts deleted file mode 100644 index 047b86c1..00000000 --- a/apps/next-app/src/services/feeds/index.ts +++ /dev/null @@ -1,103 +0,0 @@ -import { feedoongApi } from 'services/api/index' -import type { - Feed, - LikePostResponse, - PreviewResponse, - SubmitRssUrlParams, - SubmitRssUrlResponse, - SubmitViewedPost, -} from 'types/feeds' - -export const getFeeds = (page = 1, size = 10) => { - return feedoongApi({ - method: 'GET', - url: '/items', - params: { - page, - size, - }, - }) -} - -export const getChannel = (channelId: string, page = 1, size = 10) => { - return feedoongApi({ - method: 'GET', - url: `/items/channel/${channelId}`, - params: { - page, - size, - }, - }) -} - -export const checkUrlAsRss = (url: string) => { - return feedoongApi({ - method: 'GET', - url: `/channels/preview`, - params: { url }, - }) -} - -export const checkUrlAsDirectRss = ({ - homeUrl, - rssFeedUrl, -}: { - homeUrl: string - rssFeedUrl: string -}) => { - return feedoongApi({ - method: 'GET', - url: `/channels/preview/rss`, - params: { homeUrl, rssFeedUrl }, - }) -} - -export const submitRssUrl = (params: Partial) => { - if (!params.url || !params.feedUrl) { - throw new Error('url and feedUrl are required') - } - return feedoongApi({ - method: 'POST', - url: `/channels`, - data: { - ...params, - }, - }) -} - -export const likePost = (id: string) => { - return feedoongApi({ - method: 'POST', - url: `/likes/${id}`, - }) -} - -export const unlikePost = (id: string) => { - return feedoongApi({ - method: 'DELETE', - url: `/likes/${id}`, - }) -} - -export const getLikedPosts = (page: number) => { - return feedoongApi({ - method: 'GET', - url: `/items/liked`, - params: { page }, - }) -} - -export const submitViewedPost = (id: number) => { - return feedoongApi({ - method: 'POST', - url: `/items/view/${id}`, - }) -} - -export const getLikedPostsByUsername = (page: number, username?: string) => { - return feedoongApi({ - method: 'GET', - url: `/users/${username}/liked-items`, - params: { page }, - }) -} From 6b0da3e8623b96fb01ba361045184a3530df204f Mon Sep 17 00:00:00 2001 From: saengmotmi Date: Wed, 17 Jul 2024 00:03:25 +0900 Subject: [PATCH 03/14] =?UTF-8?q?fix:=20=EC=95=84=EC=A7=81=20MyFeed?= =?UTF-8?q?=EC=97=90=20=EB=AC=B8=EC=A0=9C=20=EC=9E=88=EC=9D=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/app/(hasGNB)/_components/Nav/Nav.tsx | 37 ++++----------- .../components/views/Feeds/FeedsContainer.tsx | 2 + .../components/views/Feeds/MyFeed/MyFeed.tsx | 5 +- .../Feeds/Recommended/RecommendedChannels.tsx | 6 +-- .../Feeds/Recommended/RecommendedPosts.tsx | 6 +-- .../hooks/useChannelListByUsername.ts | 13 ++++-- .../views/UserPage/UserPageContainer.utils.ts | 4 +- apps/next-app/src/entities/item/api/index.ts | 2 +- .../auth/withAuthQueryServerSideProps.ts | 4 +- apps/next-app/src/features/channel/index.ts | 4 +- .../next-app/src/features/user/userProfile.ts | 14 ++++-- apps/next-app/src/pages/oauth/index.tsx | 5 +- apps/next-app/src/services/api/index.ts | 13 ++---- apps/next-app/src/services/auth/index.ts | 46 ++++--------------- .../src/services/recommendations/index.ts | 17 ------- .../src/services/subscriptions/index.ts | 23 ---------- apps/next-app/src/views/myFeed/MyFeed.tsx | 10 ++-- 17 files changed, 67 insertions(+), 144 deletions(-) delete mode 100644 apps/next-app/src/services/recommendations/index.ts delete mode 100644 apps/next-app/src/services/subscriptions/index.ts diff --git a/apps/next-app/src/app/(hasGNB)/_components/Nav/Nav.tsx b/apps/next-app/src/app/(hasGNB)/_components/Nav/Nav.tsx index 516cae0c..78ca43f1 100644 --- a/apps/next-app/src/app/(hasGNB)/_components/Nav/Nav.tsx +++ b/apps/next-app/src/app/(hasGNB)/_components/Nav/Nav.tsx @@ -1,37 +1,18 @@ -import React, { forwardRef } from 'react' -import { cookies } from 'next/headers' +import { forwardRef } from 'react' -import ProfilePopover from 'components/common/Layout/Nav/ProfilePopover' -import { getApiEndpoint } from 'envs' import * as S from 'components/common/Layout/Nav/Nav.style' +import ProfilePopover from 'components/common/Layout/Nav/ProfilePopover' import { GoToSignUpButton } from './GoToSignUpButton' import { LogoButton } from './LogoButton' - -async function getUserProfile() { - const cookieStore = cookies() - const accessToken = cookieStore.get('accessToken') - if (!accessToken) { - return null - } - - const res = await fetch(`${getApiEndpoint()}/users/me`, { - headers: { - Authorization: `Bearer ${accessToken.value}`, - }, - }) - // The return value is *not* serialized - // You can return Date, Map, Set, etc. - - if (res.status !== 200) { - // This will activate the closest `error.js` Error Boundary - throw new Error('Failed to fetch data') - } - - return res.json() -} +import { getUserInfoUsingGET } from 'services/types/_generated/user' const Nav = forwardRef(async function Nav(props, ref) { - const userProfile = await getUserProfile() + let userProfile = null + try { + userProfile = await getUserInfoUsingGET() + } catch (error) { + console.error(error) + } return ( diff --git a/apps/next-app/src/components/views/Feeds/FeedsContainer.tsx b/apps/next-app/src/components/views/Feeds/FeedsContainer.tsx index 5c5d13f8..de1c06fa 100644 --- a/apps/next-app/src/components/views/Feeds/FeedsContainer.tsx +++ b/apps/next-app/src/components/views/Feeds/FeedsContainer.tsx @@ -1,3 +1,5 @@ +'use client' + import { useRouter } from 'next/router' import { SwitchCase } from '@toss/react' diff --git a/apps/next-app/src/components/views/Feeds/MyFeed/MyFeed.tsx b/apps/next-app/src/components/views/Feeds/MyFeed/MyFeed.tsx index 7a8007ac..0efdb8c0 100644 --- a/apps/next-app/src/components/views/Feeds/MyFeed/MyFeed.tsx +++ b/apps/next-app/src/components/views/Feeds/MyFeed/MyFeed.tsx @@ -1,4 +1,5 @@ -import { useInfiniteQuery } from '@tanstack/react-query' +'use client' +import { useSuspenseInfiniteQuery } from '@tanstack/react-query' import { useEffect } from 'react' import { useInView } from 'react-intersection-observer' @@ -11,7 +12,7 @@ import * as S from '../FeedsContainer.style' const MyFeed = () => { const { data, fetchNextPage, hasNextPage, isFetchingNextPage, isFetching } = - useInfiniteQuery({ + useSuspenseInfiniteQuery({ queryKey: CACHE_KEYS.feeds, queryFn: ({ pageParam = 1 }) => getItemsUsingGET({ diff --git a/apps/next-app/src/components/views/Feeds/Recommended/RecommendedChannels.tsx b/apps/next-app/src/components/views/Feeds/Recommended/RecommendedChannels.tsx index 187f9b16..60dad5ea 100644 --- a/apps/next-app/src/components/views/Feeds/Recommended/RecommendedChannels.tsx +++ b/apps/next-app/src/components/views/Feeds/Recommended/RecommendedChannels.tsx @@ -1,15 +1,15 @@ import { useQuery } from '@tanstack/react-query' import FeedItem from 'components/common/FeedItem' -import { CACHE_KEYS } from 'services/cacheKeys' -import { getRecommendedChannels } from 'services/recommendations' import { SkeletonChannelType } from 'components/common/Skeleton' +import { CACHE_KEYS } from 'services/cacheKeys' +import { getRecommendedChannelsUsingGET } from 'services/types/_generated/channel' import * as S from '../FeedsContainer.style' const RecommendedChannels = () => { const { data, isFetching } = useQuery({ queryKey: CACHE_KEYS.recommended(['channels']), - queryFn: getRecommendedChannels, + queryFn: getRecommendedChannelsUsingGET, }) const showSkeleton = isFetching && !data diff --git a/apps/next-app/src/components/views/Feeds/Recommended/RecommendedPosts.tsx b/apps/next-app/src/components/views/Feeds/Recommended/RecommendedPosts.tsx index 382e9e42..6ea5677a 100644 --- a/apps/next-app/src/components/views/Feeds/Recommended/RecommendedPosts.tsx +++ b/apps/next-app/src/components/views/Feeds/Recommended/RecommendedPosts.tsx @@ -1,15 +1,15 @@ import { useQuery } from '@tanstack/react-query' import FeedItem from 'components/common/FeedItem' -import { CACHE_KEYS } from 'services/cacheKeys' -import { getRecommendedPosts } from 'services/recommendations' import { SkeletonPostType } from 'components/common/Skeleton' +import { CACHE_KEYS } from 'services/cacheKeys' +import { getRecommendedItemsUsingGET } from 'services/types/_generated/item' import * as S from '../FeedsContainer.style' const RecommendedPosts = () => { const { data, isFetching } = useQuery({ queryKey: CACHE_KEYS.recommended(['posts']), - queryFn: getRecommendedPosts, + queryFn: getRecommendedItemsUsingGET, }) const showSkeleton = isFetching && !data diff --git a/apps/next-app/src/components/views/UserPage/List/ChannelList/hooks/useChannelListByUsername.ts b/apps/next-app/src/components/views/UserPage/List/ChannelList/hooks/useChannelListByUsername.ts index c7f99f24..a3653b6b 100644 --- a/apps/next-app/src/components/views/UserPage/List/ChannelList/hooks/useChannelListByUsername.ts +++ b/apps/next-app/src/components/views/UserPage/List/ChannelList/hooks/useChannelListByUsername.ts @@ -3,7 +3,8 @@ import { useRouter } from 'next/router' import { useCheckIsMyProfile } from 'features/user/useCheckIsMyProfile' import { CACHE_KEYS } from 'services/cacheKeys' -import { getChannels, getChannelsByUsername } from 'services/subscriptions' +import { getSubscriptionsUsingGET } from 'services/types/_generated/subscription' +import { getUserSubscriptionsUsingGET } from 'services/types/_generated/user' const useChannelListByUsername = (username?: string) => { const router = useRouter() @@ -14,8 +15,14 @@ const useChannelListByUsername = (username?: string) => { queryKey: [CACHE_KEYS.channels, { page: currentPage }], queryFn: () => isMyProfile - ? getChannels(currentPage) - : getChannelsByUsername(currentPage, username), + ? getSubscriptionsUsingGET({ + page: currentPage, + size: 10, + }) + : getUserSubscriptionsUsingGET(username!, { + page: currentPage, + size: 10, + }), enabled: !!username, }) diff --git a/apps/next-app/src/components/views/UserPage/UserPageContainer.utils.ts b/apps/next-app/src/components/views/UserPage/UserPageContainer.utils.ts index e1f80a68..54e9a3aa 100644 --- a/apps/next-app/src/components/views/UserPage/UserPageContainer.utils.ts +++ b/apps/next-app/src/components/views/UserPage/UserPageContainer.utils.ts @@ -1,6 +1,6 @@ import { getDomainName } from 'envs' -import type { UserProfile } from 'services/auth' +import type { PublicUserInfoResponse } from 'services/types/_generated/apiDocumentation.schemas' -export const getFeedoongUrl = (userProfile?: UserProfile) => { +export const getFeedoongUrl = (userProfile?: PublicUserInfoResponse) => { return `${getDomainName()}/${userProfile?.username}` } diff --git a/apps/next-app/src/entities/item/api/index.ts b/apps/next-app/src/entities/item/api/index.ts index e70a97af..ae80d040 100644 --- a/apps/next-app/src/entities/item/api/index.ts +++ b/apps/next-app/src/entities/item/api/index.ts @@ -7,7 +7,7 @@ export const itemQueries = { list: () => infiniteQueryOptions({ queryKey: [...itemQueries.all(), 'list'], - queryFn: ({ pageParam }) => { + queryFn: ({ pageParam = 1 }) => { return getItemsUsingGET({ page: pageParam, size: 10, diff --git a/apps/next-app/src/features/auth/withAuthQueryServerSideProps.ts b/apps/next-app/src/features/auth/withAuthQueryServerSideProps.ts index 4edaca58..db8f7e78 100644 --- a/apps/next-app/src/features/auth/withAuthQueryServerSideProps.ts +++ b/apps/next-app/src/features/auth/withAuthQueryServerSideProps.ts @@ -3,8 +3,8 @@ import type { GetServerSideProps, GetServerSidePropsContext } from 'next' import type { feedoongApi } from 'services/api' import type { UserProfile } from 'services/auth' -import { getUserInfo } from 'services/auth' import { CACHE_KEYS } from 'services/cacheKeys' +import { getUserInfoUsingGET } from 'services/types/_generated/user' export type GetServerSidePropsContextWithAuthClient = GetServerSidePropsContext & { @@ -22,7 +22,7 @@ export const withAuthQueryServerSideProps = ( await queryClient.prefetchQuery({ queryKey: CACHE_KEYS.me, - queryFn: getUserInfo, + queryFn: getUserInfoUsingGET, }) if (!getServerSidePropsFunc) { diff --git a/apps/next-app/src/features/channel/index.ts b/apps/next-app/src/features/channel/index.ts index 30cae4c9..c6ef3ace 100644 --- a/apps/next-app/src/features/channel/index.ts +++ b/apps/next-app/src/features/channel/index.ts @@ -5,8 +5,8 @@ import type { AxiosError } from 'axios' import Toast from 'components/common/Toast' import { ChannelToast } from 'components/views/RssInput/RssInputContainer.utils' import { CACHE_KEYS } from 'services/cacheKeys' -import { deleteChannel } from 'services/subscriptions' import { registerChannelUsingPOST } from 'services/types/_generated/channel' +import { unsubscribeUsingDELETE } from 'services/types/_generated/subscription' import type { Channel } from 'types/subscriptions' import type { ErrorBody } from 'utils/errors' import { getAxiosError } from 'utils/errors' @@ -50,7 +50,7 @@ export const useUnsubscribeChannel = ( const { mutate } = useMutation({ mutationKey: CACHE_KEYS.channel(item.id), - mutationFn: () => deleteChannel(item.id), + mutationFn: () => unsubscribeUsingDELETE(item.id), onSuccess: () => { Toast.show({ content: '구독이 해제되었습니다.' }) client.invalidateQueries({ predicate }) diff --git a/apps/next-app/src/features/user/userProfile.ts b/apps/next-app/src/features/user/userProfile.ts index a3907c73..0b4aea3c 100644 --- a/apps/next-app/src/features/user/userProfile.ts +++ b/apps/next-app/src/features/user/userProfile.ts @@ -3,15 +3,19 @@ import { useRouter } from 'next/router' import { getRefreshTokenFromCookie } from 'features/auth/token' import type { UserProfile } from 'services/auth' -import { getUserInfo, getUserInfoByUsername } from 'services/auth' import { CACHE_KEYS } from 'services/cacheKeys' +import type { PublicUserInfoResponse } from 'services/types/_generated/apiDocumentation.schemas' +import { + getUserInfoUsingGET, + getPublicUserInfoUsingGET, +} from 'services/types/_generated/user' export const useGetUserProfile = ( options: Omit, 'queryKey'> = {} ) => { return useQuery({ queryKey: CACHE_KEYS.me, - queryFn: getUserInfo, + queryFn: getUserInfoUsingGET, enabled: !!getRefreshTokenFromCookie(), ...options, }) @@ -19,11 +23,11 @@ export const useGetUserProfile = ( export const useGetUserProfileByUsername = ( username: string, - options: Omit, 'queryKey'> = {} + options: Omit, 'queryKey'> = {} ) => { - return useQuery({ + return useQuery({ queryKey: [CACHE_KEYS.user, username], - queryFn: () => getUserInfoByUsername(username), + queryFn: async () => getPublicUserInfoUsingGET(username), ...options, enabled: !!username, }) diff --git a/apps/next-app/src/pages/oauth/index.tsx b/apps/next-app/src/pages/oauth/index.tsx index 4ae3e2fd..e6844df5 100644 --- a/apps/next-app/src/pages/oauth/index.tsx +++ b/apps/next-app/src/pages/oauth/index.tsx @@ -4,7 +4,7 @@ import qs from 'query-string' import humps from 'humps' import { useEffect } from 'react' -import { submitAccessToken } from 'services/auth' +import { loginUsingPOST } from 'services/types/_generated/user' import { CACHE_KEYS } from 'services/cacheKeys' import { setAccessTokenToCookie, @@ -17,7 +17,8 @@ const Oauth = () => { const { data, isError } = useQuery({ queryKey: CACHE_KEYS.signup, - queryFn: () => submitAccessToken(parseAccessToken(router.asPath)), + queryFn: () => + loginUsingPOST({ accessToken: parseAccessToken(router.asPath) }), }) useEffect(() => { diff --git a/apps/next-app/src/services/api/index.ts b/apps/next-app/src/services/api/index.ts index 03d383e8..1fe7a0bc 100644 --- a/apps/next-app/src/services/api/index.ts +++ b/apps/next-app/src/services/api/index.ts @@ -19,19 +19,16 @@ export const feedoongApi = (config: AxiosRequestConfig): Promise => { baseURL: getApiEndpoint(), validateStatus: (status) => status >= httpStatus.OK && status < httpStatus.BAD_REQUEST, // 200 ~ 399 - headers: { - ...(accessToken && { Authorization: `Bearer ${accessToken}` }), - }, }) + config.headers = { + ...config.headers, + Authorization: `Bearer ${accessToken}`, + } + _api.interceptors.response.use( // try (response) => { - config.headers = { - ...config.headers, - Authorization: `Bearer ${accessToken}`, - } - return Promise.resolve( camelizeKeys(response.data) ) as unknown as AxiosResponse diff --git a/apps/next-app/src/services/auth/index.ts b/apps/next-app/src/services/auth/index.ts index 806b5ed4..cdb1adcd 100644 --- a/apps/next-app/src/services/auth/index.ts +++ b/apps/next-app/src/services/auth/index.ts @@ -1,18 +1,17 @@ -import axios, { +import type { AxiosRequestConfig } from 'axios' +import { type AxiosError, type AxiosInstance, - type AxiosResponse, type AxiosRequestHeaders, } from 'axios' -import { getApiEndpoint } from 'envs' -import { feedoongApi } from 'services/api' import { getRefreshTokenFromCookie, setAccessTokenToCookie, setRefreshTokenToCookie, } from 'features/auth/token' import type UserProfile from 'pages/[userName]' +import { reissueTokenUsingPOST } from 'services/types/_generated/user' export interface UserProfile { email: string @@ -26,54 +25,25 @@ export interface SignUpResponse extends UserProfile { refreshToken: string } -export const submitAccessToken = (token: string) => { - return feedoongApi({ - method: 'POST', - url: '/users/login/google', - data: null, - params: { accessToken: token }, - }) -} - -export const getUserInfo = () => { - return feedoongApi({ - method: 'GET', - url: '/users/me', - }) -} - -export const getUserInfoByUsername = (username: string) => { - return feedoongApi>({ - method: 'GET', - url: `/users/${username}/info`, - }) -} - export const refreshAccessToken = async ( axiosError: AxiosError, _api: AxiosInstance ) => { - const originalRequest = axiosError.config + const originalRequest = axiosError.config as AxiosRequestConfig // TODO: 리프레시 토큰 만료시 로그아웃 처리도 필요 - const { data } = await axios.post< - SignUpResponse['refreshToken'], - AxiosResponse - >(getApiEndpoint() + `/users/token`, { + const data = await reissueTokenUsingPOST({ refreshToken: getRefreshTokenFromCookie(), }) - const newAccessToken = data.accessToken - const newRefreshToken = data.refreshToken - - setRefreshTokenToCookie(newRefreshToken) - setAccessTokenToCookie(newAccessToken) + setRefreshTokenToCookie(data.refreshToken) + setAccessTokenToCookie(data.accessToken) // 필요한 코드인지 확인 필요 if (!originalRequest?.headers) { originalRequest!.headers = {} as AxiosRequestHeaders } - originalRequest!.headers.Authorization = `Bearer ${newAccessToken}` + originalRequest!.headers.Authorization = `Bearer ${data.accessToken}` // 401로 요청 실패했던 요청 새로운 accessToken으로 재요청 return _api(originalRequest!) } diff --git a/apps/next-app/src/services/recommendations/index.ts b/apps/next-app/src/services/recommendations/index.ts deleted file mode 100644 index 2d0d0cea..00000000 --- a/apps/next-app/src/services/recommendations/index.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { feedoongApi } from 'services/api' -import type { Post } from 'types/feeds' -import type { Channel } from 'types/subscriptions' - -export const getRecommendedChannels = () => { - return feedoongApi<{ channels: Channel[] }>({ - method: 'GET', - url: `/channels/recommended`, - }) -} - -export const getRecommendedPosts = () => { - return feedoongApi<{ items: Post[] }>({ - method: 'GET', - url: `/items/recommended`, - }) -} diff --git a/apps/next-app/src/services/subscriptions/index.ts b/apps/next-app/src/services/subscriptions/index.ts deleted file mode 100644 index 30fd0ab4..00000000 --- a/apps/next-app/src/services/subscriptions/index.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { feedoongApi } from 'services/api' -import type { Channels } from 'types/subscriptions' - -export const getChannels = (page: number) => { - return feedoongApi({ - url: `/subscriptions`, - params: { page }, - }) -} - -export const deleteChannel = (channelId: number) => { - return feedoongApi({ - url: `/subscriptions/${channelId}`, - method: 'DELETE', - }) -} - -export const getChannelsByUsername = (page: number, username?: string) => { - return feedoongApi({ - url: `/users/${username}/subscriptions`, - params: { page }, - }) -} diff --git a/apps/next-app/src/views/myFeed/MyFeed.tsx b/apps/next-app/src/views/myFeed/MyFeed.tsx index e4cdb5e4..ac58b115 100644 --- a/apps/next-app/src/views/myFeed/MyFeed.tsx +++ b/apps/next-app/src/views/myFeed/MyFeed.tsx @@ -1,11 +1,11 @@ 'use client' -import { useSuspenseInfiniteQuery } from '@tanstack/react-query' -import { useInView } from 'react-intersection-observer' +import { useInfiniteQuery } from '@tanstack/react-query' import { useEffect } from 'react' +import { useInView } from 'react-intersection-observer' +import Loading from 'components/common/Loading' import { itemQueries } from 'entities/item/api' import { PostFeedItem } from 'features/post/ui/PostFeedItem' -import Loading from 'components/common/Loading' import * as S from './MyFeed.style' @@ -19,7 +19,7 @@ const MyFeed = ({ isLoggedIn }: Props) => { fetchNextPage, isFetchingNextPage, hasNextPage, - } = useSuspenseInfiniteQuery(itemQueries.list()) + } = useInfiniteQuery(itemQueries.list()) const { ref, inView } = useInView({ rootMargin: '25px' }) @@ -32,7 +32,7 @@ const MyFeed = ({ isLoggedIn }: Props) => { return ( <> - {itemList.pages.map((page) => + {itemList?.pages.map((page) => page.items.map((item) => ( )) From 19c17fb46cccb14f23b84f4d36c293898e36bda5 Mon Sep 17 00:00:00 2001 From: saengmotmi Date: Wed, 17 Jul 2024 00:18:51 +0900 Subject: [PATCH 04/14] =?UTF-8?q?fix:=20=ED=94=BC=EB=93=9C=20=EC=9B=90?= =?UTF-8?q?=EC=83=81=20=EB=B3=B5=EA=B5=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/next-app/src/app/(hasGNB)/feed/me/page.tsx | 4 ++-- .../src/components/views/Feeds/MyFeed/MyFeed.tsx | 4 ++-- apps/next-app/src/entities/item/api/index.ts | 2 +- apps/next-app/src/views/myFeed/MyFeed.tsx | 10 +++------- 4 files changed, 8 insertions(+), 12 deletions(-) diff --git a/apps/next-app/src/app/(hasGNB)/feed/me/page.tsx b/apps/next-app/src/app/(hasGNB)/feed/me/page.tsx index dd5fc9ff..708af2b0 100644 --- a/apps/next-app/src/app/(hasGNB)/feed/me/page.tsx +++ b/apps/next-app/src/app/(hasGNB)/feed/me/page.tsx @@ -9,9 +9,9 @@ import { itemQueries } from 'entities/item/api' import { checkLoggedIn } from 'shared/utils/checkLoggedIn' import MyFeed from 'views/myFeed' -const FeedMePage: NextPage = () => { +const FeedMePage: NextPage = async () => { const queryClient = getQueryClient() - void queryClient.prefetchInfiniteQuery(itemQueries.list()) + await queryClient.prefetchInfiniteQuery(itemQueries.list()) return ( diff --git a/apps/next-app/src/components/views/Feeds/MyFeed/MyFeed.tsx b/apps/next-app/src/components/views/Feeds/MyFeed/MyFeed.tsx index 0efdb8c0..f69c95d5 100644 --- a/apps/next-app/src/components/views/Feeds/MyFeed/MyFeed.tsx +++ b/apps/next-app/src/components/views/Feeds/MyFeed/MyFeed.tsx @@ -1,5 +1,5 @@ 'use client' -import { useSuspenseInfiniteQuery } from '@tanstack/react-query' +import { useInfiniteQuery } from '@tanstack/react-query' import { useEffect } from 'react' import { useInView } from 'react-intersection-observer' @@ -12,7 +12,7 @@ import * as S from '../FeedsContainer.style' const MyFeed = () => { const { data, fetchNextPage, hasNextPage, isFetchingNextPage, isFetching } = - useSuspenseInfiniteQuery({ + useInfiniteQuery({ queryKey: CACHE_KEYS.feeds, queryFn: ({ pageParam = 1 }) => getItemsUsingGET({ diff --git a/apps/next-app/src/entities/item/api/index.ts b/apps/next-app/src/entities/item/api/index.ts index ae80d040..513c6bc2 100644 --- a/apps/next-app/src/entities/item/api/index.ts +++ b/apps/next-app/src/entities/item/api/index.ts @@ -15,7 +15,7 @@ export const itemQueries = { }, initialPageParam: 1, getNextPageParam: (lastPage) => { - return lastPage.items.length === 10 ? lastPage.next : undefined + return lastPage.next }, }), } diff --git a/apps/next-app/src/views/myFeed/MyFeed.tsx b/apps/next-app/src/views/myFeed/MyFeed.tsx index ac58b115..8c71f348 100644 --- a/apps/next-app/src/views/myFeed/MyFeed.tsx +++ b/apps/next-app/src/views/myFeed/MyFeed.tsx @@ -14,12 +14,8 @@ interface Props { } const MyFeed = ({ isLoggedIn }: Props) => { - const { - data: itemList, - fetchNextPage, - isFetchingNextPage, - hasNextPage, - } = useInfiniteQuery(itemQueries.list()) + const { data, fetchNextPage, isFetchingNextPage, hasNextPage } = + useInfiniteQuery(itemQueries.list()) const { ref, inView } = useInView({ rootMargin: '25px' }) @@ -32,7 +28,7 @@ const MyFeed = ({ isLoggedIn }: Props) => { return ( <> - {itemList?.pages.map((page) => + {data?.pages.map((page) => page.items.map((item) => ( )) From 451151d121f432d93730f9a49630c71265ca9fbc Mon Sep 17 00:00:00 2001 From: saengmotmi Date: Wed, 17 Jul 2024 00:20:27 +0900 Subject: [PATCH 05/14] fix: nav --- .../src/app/(hasGNB)/_components/Nav/Nav.tsx | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/apps/next-app/src/app/(hasGNB)/_components/Nav/Nav.tsx b/apps/next-app/src/app/(hasGNB)/_components/Nav/Nav.tsx index 78ca43f1..fbf1e764 100644 --- a/apps/next-app/src/app/(hasGNB)/_components/Nav/Nav.tsx +++ b/apps/next-app/src/app/(hasGNB)/_components/Nav/Nav.tsx @@ -1,18 +1,15 @@ +'use client' import { forwardRef } from 'react' import * as S from 'components/common/Layout/Nav/Nav.style' import ProfilePopover from 'components/common/Layout/Nav/ProfilePopover' +import { useGetUserProfile } from 'features/user/userProfile' import { GoToSignUpButton } from './GoToSignUpButton' import { LogoButton } from './LogoButton' -import { getUserInfoUsingGET } from 'services/types/_generated/user' -const Nav = forwardRef(async function Nav(props, ref) { - let userProfile = null - try { - userProfile = await getUserInfoUsingGET() - } catch (error) { - console.error(error) - } +// NOTE: 서버 컴포넌트로 만들면 클라 측에서 갱신이 안됨 +const Nav = forwardRef(function Nav(props, ref) { + const { data: userProfile } = useGetUserProfile() return ( From 3e00a931c1a5efc5953139c940dd1498fce88265 Mon Sep 17 00:00:00 2001 From: saengmotmi Date: Mon, 29 Jul 2024 00:28:18 +0900 Subject: [PATCH 06/14] =?UTF-8?q?fix:=20app=20router=EC=9A=A9=20=EC=BF=A0?= =?UTF-8?q?=ED=82=A4=20=EB=A1=9C=EC=A7=81=20-=20auth=20wip?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../views/UserPage/List/PostList/PostList.tsx | 20 ++++---- apps/next-app/src/features/auth/token.ts | 51 ++++++++++++++++--- .../next-app/src/features/user/userProfile.ts | 1 + apps/next-app/src/services/api/index.ts | 2 +- apps/next-app/src/views/myFeed/MyFeed.tsx | 4 +- 5 files changed, 59 insertions(+), 19 deletions(-) diff --git a/apps/next-app/src/components/views/UserPage/List/PostList/PostList.tsx b/apps/next-app/src/components/views/UserPage/List/PostList/PostList.tsx index 418146bf..e47409e9 100644 --- a/apps/next-app/src/components/views/UserPage/List/PostList/PostList.tsx +++ b/apps/next-app/src/components/views/UserPage/List/PostList/PostList.tsx @@ -1,20 +1,22 @@ import { useRouter } from 'next/router' -import { ITEMS_PER_PAGE } from 'constants/pagination' -import List from '..' -import usePostListByUsername from './hooks/usePostListByUsername' +import EmptyContents from 'components/common/EmptyContents' +import FeedItem from 'components/common/FeedItem' import Flex from 'components/common/Flex' import Paging from 'components/common/Paging' -import EmptyContents from 'components/common/EmptyContents' import { SkeletonPostType } from 'components/common/Skeleton' -import FeedItem from 'components/common/FeedItem' -import { useGetUsernameFromPath } from 'features/user/userProfile' -import { getRefreshTokenFromCookie } from 'features/auth/token' +import { ITEMS_PER_PAGE } from 'constants/pagination' +import { + useGetUsernameFromPath, + useGetUserProfile, +} from 'features/user/userProfile' +import List from '..' +import usePostListByUsername from './hooks/usePostListByUsername' const PostList = () => { const router = useRouter() const username = useGetUsernameFromPath() - const isSomeoneLoggedIn = !!getRefreshTokenFromCookie() + const { data: me } = useGetUserProfile() const { listData, isLoading, isEmptyList, totalCount } = usePostListByUsername(username) @@ -34,7 +36,7 @@ const PostList = () => { key={item.id} type="post" item={item} - isPrivate={isSomeoneLoggedIn} + isPrivate={!!me} /> )) } diff --git a/apps/next-app/src/features/auth/token.ts b/apps/next-app/src/features/auth/token.ts index 9f2e6f36..a7ec120a 100644 --- a/apps/next-app/src/features/auth/token.ts +++ b/apps/next-app/src/features/auth/token.ts @@ -2,24 +2,61 @@ import dayjs from 'dayjs' import Cookies from 'js-cookie' import { AccessToken, RefreshToken } from 'constants/auth' +import { isServer } from 'utils' + +const isAppRouter = () => { + try { + // Throws an error if we are not in the App Router context. + // eslint-disable-next-line @typescript-eslint/no-var-requires + const { cookies } = require('next/headers') + cookies() + return true + } catch (e) { + return false + } +} + +// eslint-disable-next-line @typescript-eslint/no-var-requires +export const getNextCookies = () => require('next/headers').cookies() +// import('next/headers').then((res) => res.cookies()) + +const getIsomorphicCookies = () => + isServer() && isAppRouter() ? getNextCookies() : Cookies export const getRefreshTokenFromCookie = () => { - return Cookies.get(RefreshToken) + const cookies = getIsomorphicCookies() + const token = cookies.get(RefreshToken) + + if (token instanceof Object && 'value' in token) { + return token.value + } + return token } -export const setRefreshTokenToCookie = (token: string) => { - Cookies.set(RefreshToken, token, { +export const setRefreshTokenToCookie = async (token: string) => { + const cookies = await getIsomorphicCookies() + + cookies.set(RefreshToken, token, { expires: dayjs().add(6, 'month').toDate(), sameSite: 'lax', }) } -export const getAccessTokenFromCookie = () => { - return Cookies.get(AccessToken) +export const getAccessTokenFromCookie = async () => { + const cookies = await getIsomorphicCookies() + const token = cookies.get(RefreshToken) + + if (token instanceof Object && 'value' in token) { + return token.value + } + + return cookies.get(AccessToken) } -export const setAccessTokenToCookie = (token: string) => { - Cookies.set(AccessToken, token, { +export const setAccessTokenToCookie = async (token: string) => { + const cookies = await getIsomorphicCookies() + + cookies.set(AccessToken, token, { expires: dayjs().add(1, 'month').toDate(), secure: true, sameSite: 'lax', diff --git a/apps/next-app/src/features/user/userProfile.ts b/apps/next-app/src/features/user/userProfile.ts index 0b4aea3c..2072f87d 100644 --- a/apps/next-app/src/features/user/userProfile.ts +++ b/apps/next-app/src/features/user/userProfile.ts @@ -16,6 +16,7 @@ export const useGetUserProfile = ( return useQuery({ queryKey: CACHE_KEYS.me, queryFn: getUserInfoUsingGET, + // NOTE: 왜 액세스 토큰이 아니라 리프레시 토큰? enabled: !!getRefreshTokenFromCookie(), ...options, }) diff --git a/apps/next-app/src/services/api/index.ts b/apps/next-app/src/services/api/index.ts index 1fe7a0bc..3d2f1490 100644 --- a/apps/next-app/src/services/api/index.ts +++ b/apps/next-app/src/services/api/index.ts @@ -42,7 +42,7 @@ export const feedoongApi = (config: AxiosRequestConfig): Promise => { [httpStatus.UNAUTHORIZED, httpStatus.FORBIDDEN].includes(errorStatus) ) { // 리프레시 토큰이 없을 경우 로그인 페이지로 리다이렉트 시켜야 함 - if (getRefreshTokenFromCookie()) { + if (await getRefreshTokenFromCookie()) { return refreshAccessToken(error, _api) } } diff --git a/apps/next-app/src/views/myFeed/MyFeed.tsx b/apps/next-app/src/views/myFeed/MyFeed.tsx index 8c71f348..b4f4d8b1 100644 --- a/apps/next-app/src/views/myFeed/MyFeed.tsx +++ b/apps/next-app/src/views/myFeed/MyFeed.tsx @@ -1,5 +1,5 @@ 'use client' -import { useInfiniteQuery } from '@tanstack/react-query' +import { useSuspenseInfiniteQuery } from '@tanstack/react-query' import { useEffect } from 'react' import { useInView } from 'react-intersection-observer' @@ -15,7 +15,7 @@ interface Props { const MyFeed = ({ isLoggedIn }: Props) => { const { data, fetchNextPage, isFetchingNextPage, hasNextPage } = - useInfiniteQuery(itemQueries.list()) + useSuspenseInfiniteQuery(itemQueries.list()) const { ref, inView } = useInView({ rootMargin: '25px' }) From ec0dbd088c79b0397abf3355e9b8cdbb1a94dd1a Mon Sep 17 00:00:00 2001 From: saengmotmi Date: Mon, 29 Jul 2024 00:40:39 +0900 Subject: [PATCH 07/14] =?UTF-8?q?fix:=20=EC=9E=98=EB=AA=BB=EB=90=9C=20?= =?UTF-8?q?=ED=86=A0=ED=81=B0=20=EC=A3=BC=EC=9E=85=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/next-app/src/features/auth/token.ts | 12 ++++++------ apps/next-app/src/services/api/index.ts | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/apps/next-app/src/features/auth/token.ts b/apps/next-app/src/features/auth/token.ts index a7ec120a..7b962c55 100644 --- a/apps/next-app/src/features/auth/token.ts +++ b/apps/next-app/src/features/auth/token.ts @@ -33,8 +33,8 @@ export const getRefreshTokenFromCookie = () => { return token } -export const setRefreshTokenToCookie = async (token: string) => { - const cookies = await getIsomorphicCookies() +export const setRefreshTokenToCookie = (token: string) => { + const cookies = getIsomorphicCookies() cookies.set(RefreshToken, token, { expires: dayjs().add(6, 'month').toDate(), @@ -42,8 +42,8 @@ export const setRefreshTokenToCookie = async (token: string) => { }) } -export const getAccessTokenFromCookie = async () => { - const cookies = await getIsomorphicCookies() +export const getAccessTokenFromCookie = () => { + const cookies = getIsomorphicCookies() const token = cookies.get(RefreshToken) if (token instanceof Object && 'value' in token) { @@ -53,8 +53,8 @@ export const getAccessTokenFromCookie = async () => { return cookies.get(AccessToken) } -export const setAccessTokenToCookie = async (token: string) => { - const cookies = await getIsomorphicCookies() +export const setAccessTokenToCookie = (token: string) => { + const cookies = getIsomorphicCookies() cookies.set(AccessToken, token, { expires: dayjs().add(1, 'month').toDate(), diff --git a/apps/next-app/src/services/api/index.ts b/apps/next-app/src/services/api/index.ts index 3d2f1490..efa7b893 100644 --- a/apps/next-app/src/services/api/index.ts +++ b/apps/next-app/src/services/api/index.ts @@ -23,7 +23,7 @@ export const feedoongApi = (config: AxiosRequestConfig): Promise => { config.headers = { ...config.headers, - Authorization: `Bearer ${accessToken}`, + Authorization: accessToken ? `Bearer ${accessToken}` : undefined, } _api.interceptors.response.use( From cf007253d3137213c131183b412c5b1dffe6313e Mon Sep 17 00:00:00 2001 From: saengmotmi Date: Mon, 29 Jul 2024 00:53:30 +0900 Subject: [PATCH 08/14] =?UTF-8?q?fix:=20nav=20=ED=94=84=EB=A1=9C=ED=95=84?= =?UTF-8?q?=20=EC=98=81=EC=97=AD=20=EA=B9=9C=EB=B9=A1=EC=9E=84=20=EB=AC=B8?= =?UTF-8?q?=EC=A0=9C=20=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/app/(hasGNB)/_components/Nav/Nav.tsx | 22 +++++++---- .../src/components/common/Layout/Nav/Nav.tsx | 31 ++++------------ .../components/common/Layout/Nav/Profile.tsx | 37 +++++++++++++++++++ 3 files changed, 59 insertions(+), 31 deletions(-) create mode 100644 apps/next-app/src/components/common/Layout/Nav/Profile.tsx diff --git a/apps/next-app/src/app/(hasGNB)/_components/Nav/Nav.tsx b/apps/next-app/src/app/(hasGNB)/_components/Nav/Nav.tsx index fbf1e764..a961e089 100644 --- a/apps/next-app/src/app/(hasGNB)/_components/Nav/Nav.tsx +++ b/apps/next-app/src/app/(hasGNB)/_components/Nav/Nav.tsx @@ -1,21 +1,29 @@ 'use client' -import { forwardRef } from 'react' +import { forwardRef, Suspense } from 'react' import * as S from 'components/common/Layout/Nav/Nav.style' -import ProfilePopover from 'components/common/Layout/Nav/ProfilePopover' -import { useGetUserProfile } from 'features/user/userProfile' -import { GoToSignUpButton } from './GoToSignUpButton' +// import ProfilePopover from 'components/common/Layout/Nav/ProfilePopover' +// import { useGetUserProfile } from 'features/user/userProfile' +// import { GoToSignUpButton } from './GoToSignUpButton' import { LogoButton } from './LogoButton' +import { Profile } from 'components/common/Layout/Nav/Profile' +import { getRefreshTokenFromCookie } from 'features/auth/token' // NOTE: 서버 컴포넌트로 만들면 클라 측에서 갱신이 안됨 const Nav = forwardRef(function Nav(props, ref) { - const { data: userProfile } = useGetUserProfile() + // const { data: userProfile } = useGetUserProfile() return ( - {userProfile?.name ? ( + {getRefreshTokenFromCookie() && ( + + + + )} + + {/* {userProfile?.name ? ( {`${userProfile.name}님, 안녕하세요!`} @@ -32,7 +40,7 @@ const Nav = forwardRef(function Nav(props, ref) { ) : ( - )} + )} */} ) }) diff --git a/apps/next-app/src/components/common/Layout/Nav/Nav.tsx b/apps/next-app/src/components/common/Layout/Nav/Nav.tsx index 643de00d..ac1727cb 100644 --- a/apps/next-app/src/components/common/Layout/Nav/Nav.tsx +++ b/apps/next-app/src/components/common/Layout/Nav/Nav.tsx @@ -1,18 +1,16 @@ 'use client' -import React, { forwardRef } from 'react' import { useRouter } from 'next/navigation' +import { forwardRef, Suspense } from 'react' -import { useGetUserProfile } from 'features/user/userProfile' -import ProfilePopover from './ProfilePopover' -import { ROUTE } from 'constants/route' import LogoDesktopNoBackground from 'components/common/LogoDesktop' +import { Profile } from './Profile' +import { getRefreshTokenFromCookie } from 'features/auth/token' import * as S from './Nav.style' const Nav = forwardRef(function TopNavBar(props, ref) { const router = useRouter() - const { data: userProfile } = useGetUserProfile() return ( @@ -21,25 +19,10 @@ const Nav = forwardRef(function TopNavBar(props, ref) { Feedoong - {userProfile?.name ? ( - - - {`${userProfile.name}님, 안녕하세요!`} - {userProfile.profileImageUrl && ( - - )} - - - ) : ( - router.push(ROUTE.SIGN_UP)}> - 피둥 시작하기 - + {getRefreshTokenFromCookie() && ( + + + )} ) diff --git a/apps/next-app/src/components/common/Layout/Nav/Profile.tsx b/apps/next-app/src/components/common/Layout/Nav/Profile.tsx new file mode 100644 index 00000000..8c4dcc0b --- /dev/null +++ b/apps/next-app/src/components/common/Layout/Nav/Profile.tsx @@ -0,0 +1,37 @@ +import { useSuspenseQuery } from '@tanstack/react-query' +import router from 'next/router' + +import { ROUTE } from 'constants/route' +import { CACHE_KEYS } from 'services/cacheKeys' +import { getUserInfoUsingGET } from 'services/types/_generated/user' +import ProfilePopover from './ProfilePopover' + +import * as S from './Nav.style' + +export const Profile = () => { + const { data: userProfile } = useSuspenseQuery({ + queryKey: CACHE_KEYS.me, + queryFn: getUserInfoUsingGET, + }) + + return userProfile?.name ? ( + + + {`${userProfile.name}님, 안녕하세요!`} + {userProfile.profileImageUrl && ( + + )} + + + ) : ( + router.push(ROUTE.SIGN_UP)}> + 피둥 시작하기 + + ) +} From 13278734ca72327a2e9249541680ff48db1f7533 Mon Sep 17 00:00:00 2001 From: saengmotmi Date: Mon, 29 Jul 2024 00:54:28 +0900 Subject: [PATCH 09/14] =?UTF-8?q?fix:=20=EC=B9=B4=EB=93=9C=20=EA=B0=84?= =?UTF-8?q?=EA=B2=A9=20=EC=A1=B0=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/next-app/src/components/common/FeedItem/Post/Post.style.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/apps/next-app/src/components/common/FeedItem/Post/Post.style.tsx b/apps/next-app/src/components/common/FeedItem/Post/Post.style.tsx index 281560ef..d3281356 100644 --- a/apps/next-app/src/components/common/FeedItem/Post/Post.style.tsx +++ b/apps/next-app/src/components/common/FeedItem/Post/Post.style.tsx @@ -11,7 +11,6 @@ export const Container = styled.div` gap: 12px; border-radius: 20px; border-bottom-left-radius: 0px; - margin-bottom: 20px; ` export const Title = styled.h2` From 3879583c331826653d4d520c931cddaa3e9d77fb Mon Sep 17 00:00:00 2001 From: saengmotmi Date: Mon, 29 Jul 2024 00:55:56 +0900 Subject: [PATCH 10/14] =?UTF-8?q?refactor:=20=EB=B6=88=ED=95=84=EC=9A=94?= =?UTF-8?q?=ED=95=9C=20=EC=BD=94=EB=93=9C=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/app/(hasGNB)/_components/Nav/Nav.tsx | 49 +++++++------------ .../src/components/common/Layout/Nav/Nav.tsx | 24 ++++++--- .../components/common/Layout/Nav/Profile.tsx | 1 + apps/next-app/src/services/api/index.ts | 2 +- 4 files changed, 38 insertions(+), 38 deletions(-) diff --git a/apps/next-app/src/app/(hasGNB)/_components/Nav/Nav.tsx b/apps/next-app/src/app/(hasGNB)/_components/Nav/Nav.tsx index a961e089..3016b04b 100644 --- a/apps/next-app/src/app/(hasGNB)/_components/Nav/Nav.tsx +++ b/apps/next-app/src/app/(hasGNB)/_components/Nav/Nav.tsx @@ -1,46 +1,35 @@ 'use client' +import { usePathname, useRouter } from 'next/navigation' import { forwardRef, Suspense } from 'react' import * as S from 'components/common/Layout/Nav/Nav.style' -// import ProfilePopover from 'components/common/Layout/Nav/ProfilePopover' -// import { useGetUserProfile } from 'features/user/userProfile' -// import { GoToSignUpButton } from './GoToSignUpButton' -import { LogoButton } from './LogoButton' import { Profile } from 'components/common/Layout/Nav/Profile' -import { getRefreshTokenFromCookie } from 'features/auth/token' +import { ROUTE } from 'constants/route' +import { getAccessTokenFromCookie } from 'features/auth/token' +import { LogoButton } from './LogoButton' + +const enableGoToSignUpButton = (pathname: string | null) => + pathname === ROUTE.SIGN_UP || pathname === ROUTE.INTRODUCE // NOTE: 서버 컴포넌트로 만들면 클라 측에서 갱신이 안됨 const Nav = forwardRef(function Nav(props, ref) { - // const { data: userProfile } = useGetUserProfile() + const pathname = usePathname() + const router = useRouter() return ( - - {getRefreshTokenFromCookie() && ( - - - - )} - - {/* {userProfile?.name ? ( - - - {`${userProfile.name}님, 안녕하세요!`} - {userProfile.profileImageUrl && ( - - )} - - + {enableGoToSignUpButton(pathname) ? ( + router.push(ROUTE.SIGN_UP)}> + 피둥 시작하기 + ) : ( - - )} */} + getAccessTokenFromCookie() && ( + + + + ) + )} ) }) diff --git a/apps/next-app/src/components/common/Layout/Nav/Nav.tsx b/apps/next-app/src/components/common/Layout/Nav/Nav.tsx index ac1727cb..0797be05 100644 --- a/apps/next-app/src/components/common/Layout/Nav/Nav.tsx +++ b/apps/next-app/src/components/common/Layout/Nav/Nav.tsx @@ -1,16 +1,21 @@ 'use client' -import { useRouter } from 'next/navigation' +import { usePathname, useRouter } from 'next/navigation' import { forwardRef, Suspense } from 'react' import LogoDesktopNoBackground from 'components/common/LogoDesktop' +import { ROUTE } from 'constants/route' +import { getAccessTokenFromCookie } from 'features/auth/token' import { Profile } from './Profile' -import { getRefreshTokenFromCookie } from 'features/auth/token' import * as S from './Nav.style' +const enableGoToSignUpButton = (pathname: string | null) => + pathname === ROUTE.SIGN_UP || pathname === ROUTE.INTRODUCE + const Nav = forwardRef(function TopNavBar(props, ref) { const router = useRouter() + const pathname = usePathname() return ( @@ -18,11 +23,16 @@ const Nav = forwardRef(function TopNavBar(props, ref) { Feedoong - - {getRefreshTokenFromCookie() && ( - - - + {enableGoToSignUpButton(pathname) ? ( + router.push(ROUTE.SIGN_UP)}> + 피둥 시작하기 + + ) : ( + getAccessTokenFromCookie() && ( + + + + ) )} ) diff --git a/apps/next-app/src/components/common/Layout/Nav/Profile.tsx b/apps/next-app/src/components/common/Layout/Nav/Profile.tsx index 8c4dcc0b..8b93c407 100644 --- a/apps/next-app/src/components/common/Layout/Nav/Profile.tsx +++ b/apps/next-app/src/components/common/Layout/Nav/Profile.tsx @@ -1,3 +1,4 @@ +'use client' import { useSuspenseQuery } from '@tanstack/react-query' import router from 'next/router' diff --git a/apps/next-app/src/services/api/index.ts b/apps/next-app/src/services/api/index.ts index efa7b893..c07600a0 100644 --- a/apps/next-app/src/services/api/index.ts +++ b/apps/next-app/src/services/api/index.ts @@ -42,7 +42,7 @@ export const feedoongApi = (config: AxiosRequestConfig): Promise => { [httpStatus.UNAUTHORIZED, httpStatus.FORBIDDEN].includes(errorStatus) ) { // 리프레시 토큰이 없을 경우 로그인 페이지로 리다이렉트 시켜야 함 - if (await getRefreshTokenFromCookie()) { + if (getRefreshTokenFromCookie()) { return refreshAccessToken(error, _api) } } From ee35189ad99a8f8625f31df074d64619bbf6a21d Mon Sep 17 00:00:00 2001 From: saengmotmi Date: Mon, 29 Jul 2024 01:22:08 +0900 Subject: [PATCH 11/14] =?UTF-8?q?refactor:=20=ED=86=A0=ED=81=B0=20?= =?UTF-8?q?=EA=B4=80=EB=A0=A8=20=EB=A1=9C=EC=A7=81=20=EC=A0=95=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/next-app/src/features/auth/token.ts | 37 +++++++----------------- apps/next-app/src/shared/libs/nextjs.ts | 14 +++++++++ 2 files changed, 25 insertions(+), 26 deletions(-) create mode 100644 apps/next-app/src/shared/libs/nextjs.ts diff --git a/apps/next-app/src/features/auth/token.ts b/apps/next-app/src/features/auth/token.ts index 7b962c55..660c76a4 100644 --- a/apps/next-app/src/features/auth/token.ts +++ b/apps/next-app/src/features/auth/token.ts @@ -3,36 +3,25 @@ import Cookies from 'js-cookie' import { AccessToken, RefreshToken } from 'constants/auth' import { isServer } from 'utils' - -const isAppRouter = () => { - try { - // Throws an error if we are not in the App Router context. - // eslint-disable-next-line @typescript-eslint/no-var-requires - const { cookies } = require('next/headers') - cookies() - return true - } catch (e) { - return false - } -} - -// eslint-disable-next-line @typescript-eslint/no-var-requires -export const getNextCookies = () => require('next/headers').cookies() -// import('next/headers').then((res) => res.cookies()) +import { getNextCookies, isAppRouter } from 'shared/libs/nextjs' const getIsomorphicCookies = () => isServer() && isAppRouter() ? getNextCookies() : Cookies -export const getRefreshTokenFromCookie = () => { - const cookies = getIsomorphicCookies() - const token = cookies.get(RefreshToken) - +const getIsomorphicToken = (token: ReturnType) => { if (token instanceof Object && 'value' in token) { return token.value } return token } +export const getRefreshTokenFromCookie = () => { + const cookies = getIsomorphicCookies() + const token = getIsomorphicToken(cookies.get(RefreshToken)) + + return token +} + export const setRefreshTokenToCookie = (token: string) => { const cookies = getIsomorphicCookies() @@ -44,13 +33,9 @@ export const setRefreshTokenToCookie = (token: string) => { export const getAccessTokenFromCookie = () => { const cookies = getIsomorphicCookies() - const token = cookies.get(RefreshToken) + const token = getIsomorphicToken(cookies.get(AccessToken)) - if (token instanceof Object && 'value' in token) { - return token.value - } - - return cookies.get(AccessToken) + return token } export const setAccessTokenToCookie = (token: string) => { diff --git a/apps/next-app/src/shared/libs/nextjs.ts b/apps/next-app/src/shared/libs/nextjs.ts new file mode 100644 index 00000000..2386a0e8 --- /dev/null +++ b/apps/next-app/src/shared/libs/nextjs.ts @@ -0,0 +1,14 @@ +export const isAppRouter = () => { + try { + // Throws an error if we are not in the App Router context. + // eslint-disable-next-line @typescript-eslint/no-var-requires + const { cookies } = require('next/headers') + cookies() + return true + } catch (e) { + return false + } +} + +// eslint-disable-next-line @typescript-eslint/no-var-requires +export const getNextCookies = () => require('next/headers').cookies() From 186b9e9dcbd1a1318562ab46a957cde5bed03418 Mon Sep 17 00:00:00 2001 From: saengmotmi Date: Mon, 29 Jul 2024 01:23:46 +0900 Subject: [PATCH 12/14] =?UTF-8?q?refactor:=20=ED=94=84=EB=A1=9C=ED=95=84?= =?UTF-8?q?=20=EC=BB=B4=ED=8F=AC=EB=84=8C=ED=8A=B8=20=EC=A0=95=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../next-app/src/components/common/Layout/Nav/Profile.tsx | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/apps/next-app/src/components/common/Layout/Nav/Profile.tsx b/apps/next-app/src/components/common/Layout/Nav/Profile.tsx index 8b93c407..655e7070 100644 --- a/apps/next-app/src/components/common/Layout/Nav/Profile.tsx +++ b/apps/next-app/src/components/common/Layout/Nav/Profile.tsx @@ -1,8 +1,6 @@ 'use client' import { useSuspenseQuery } from '@tanstack/react-query' -import router from 'next/router' -import { ROUTE } from 'constants/route' import { CACHE_KEYS } from 'services/cacheKeys' import { getUserInfoUsingGET } from 'services/types/_generated/user' import ProfilePopover from './ProfilePopover' @@ -15,7 +13,7 @@ export const Profile = () => { queryFn: getUserInfoUsingGET, }) - return userProfile?.name ? ( + return ( {`${userProfile.name}님, 안녕하세요!`} @@ -30,9 +28,5 @@ export const Profile = () => { )} - ) : ( - router.push(ROUTE.SIGN_UP)}> - 피둥 시작하기 - ) } From fb4c782270cc3e0072082472202b9e75fa449074 Mon Sep 17 00:00:00 2001 From: saengmotmi Date: Mon, 29 Jul 2024 21:13:35 +0900 Subject: [PATCH 13/14] =?UTF-8?q?chore:=20=EB=B6=88=ED=95=84=EC=9A=94?= =?UTF-8?q?=ED=95=9C=20=EB=A1=9C=EC=A7=81=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/next-app/src/features/user/userProfile.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/next-app/src/features/user/userProfile.ts b/apps/next-app/src/features/user/userProfile.ts index 2072f87d..f0389337 100644 --- a/apps/next-app/src/features/user/userProfile.ts +++ b/apps/next-app/src/features/user/userProfile.ts @@ -28,7 +28,7 @@ export const useGetUserProfileByUsername = ( ) => { return useQuery({ queryKey: [CACHE_KEYS.user, username], - queryFn: async () => getPublicUserInfoUsingGET(username), + queryFn: () => getPublicUserInfoUsingGET(username), ...options, enabled: !!username, }) From 9a2e9833566c7fd7bfa953322f519085f577d146 Mon Sep 17 00:00:00 2001 From: saengmotmi Date: Tue, 30 Jul 2024 04:42:27 +0900 Subject: [PATCH 14/14] =?UTF-8?q?fix:=20=EB=B9=84=20=EB=A1=9C=EA=B7=B8?= =?UTF-8?q?=EC=9D=B8=20=EC=83=81=ED=83=9C=EC=97=90=EC=84=9C=EB=8F=84=20?= =?UTF-8?q?=EC=A0=95=EC=83=81=20=EB=8F=99=EC=9E=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .pnp.cjs | 46 ++++++++++++++++ apps/next-app/package.json | 1 + .../src/app/(hasGNB)/_components/Nav/Nav.tsx | 31 +++++------ .../src/components/common/Layout/Nav/Nav.tsx | 33 ++++++------ .../components/common/Layout/Nav/Profile.tsx | 2 + .../src/components/common/Providers/index.tsx | 3 +- apps/next-app/src/constants/route.ts | 6 +++ apps/next-app/src/features/auth/token.ts | 29 +++++++++- .../auth/withAuthQueryServerSideProps.ts | 54 ++++++++++++++++--- .../errors/globalQueryErrorHandler.ts | 27 +++++++--- apps/next-app/src/pages/[userName].tsx | 3 ++ apps/next-app/src/pages/channels/[id].tsx | 3 ++ .../pages/feed/recommended/channels/index.tsx | 5 +- .../pages/feed/recommended/posts/index.tsx | 9 +++- apps/next-app/src/shared/libs/context.ts | 10 ++++ yarn.lock | 21 ++++++++ 16 files changed, 228 insertions(+), 55 deletions(-) create mode 100644 apps/next-app/src/shared/libs/context.ts diff --git a/.pnp.cjs b/.pnp.cjs index f7abdfee..e8aee9c2 100755 --- a/.pnp.cjs +++ b/.pnp.cjs @@ -1459,6 +1459,51 @@ const RAW_RUNTIME_STATE = "linkType": "HARD"\ }]\ ]],\ + ["@suspensive/react", [\ + ["npm:2.10.0", {\ + "packageLocation": "../../../.yarn/berry/cache/@suspensive-react-npm-2.10.0-2fa3f22e20-10c0.zip/node_modules/@suspensive/react/",\ + "packageDependencies": [\ + ["@suspensive/react", "npm:2.10.0"]\ + ],\ + "linkType": "SOFT"\ + }],\ + ["virtual:f29d69f85062a03ed7bd609b4d40e9adc04add82ffc06c75c0a45531f39eb1379ffd22a0748bbd55406686765a1c241fd4e759a24923460124aed6e3dd6c4096#npm:2.10.0", {\ + "packageLocation": "./.yarn/__virtual__/@suspensive-react-virtual-bcbbe283cc/4/.yarn/berry/cache/@suspensive-react-npm-2.10.0-2fa3f22e20-10c0.zip/node_modules/@suspensive/react/",\ + "packageDependencies": [\ + ["@suspensive/react", "virtual:f29d69f85062a03ed7bd609b4d40e9adc04add82ffc06c75c0a45531f39eb1379ffd22a0748bbd55406686765a1c241fd4e759a24923460124aed6e3dd6c4096#npm:2.10.0"],\ + ["@suspensive/utils", "virtual:bcbbe283cc81330f6f2e6a3f10e2593e03c4d5827416e1efd9022e5b10be6e1e11a58c9873a35e5083dd0c70e019e3c5669244e9d8a4ca8ffb8681e6f089eeb3#npm:2.10.0"],\ + ["@types/react", "npm:18.3.1"],\ + ["react", "npm:18.3.1"]\ + ],\ + "packagePeers": [\ + "@types/react",\ + "react"\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ + ["@suspensive/utils", [\ + ["npm:2.10.0", {\ + "packageLocation": "../../../.yarn/berry/cache/@suspensive-utils-npm-2.10.0-3d65480029-10c0.zip/node_modules/@suspensive/utils/",\ + "packageDependencies": [\ + ["@suspensive/utils", "npm:2.10.0"]\ + ],\ + "linkType": "SOFT"\ + }],\ + ["virtual:bcbbe283cc81330f6f2e6a3f10e2593e03c4d5827416e1efd9022e5b10be6e1e11a58c9873a35e5083dd0c70e019e3c5669244e9d8a4ca8ffb8681e6f089eeb3#npm:2.10.0", {\ + "packageLocation": "./.yarn/__virtual__/@suspensive-utils-virtual-4629b5310a/4/.yarn/berry/cache/@suspensive-utils-npm-2.10.0-3d65480029-10c0.zip/node_modules/@suspensive/utils/",\ + "packageDependencies": [\ + ["@suspensive/utils", "virtual:bcbbe283cc81330f6f2e6a3f10e2593e03c4d5827416e1efd9022e5b10be6e1e11a58c9873a35e5083dd0c70e019e3c5669244e9d8a4ca8ffb8681e6f089eeb3#npm:2.10.0"],\ + ["@types/react", "npm:18.3.1"],\ + ["react", "npm:18.3.1"]\ + ],\ + "packagePeers": [\ + "@types/react",\ + "react"\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ ["@swc/counter", [\ ["npm:0.1.3", {\ "packageLocation": "../../../.yarn/berry/cache/@swc-counter-npm-0.1.3-ce42b0e3f5-10c0.zip/node_modules/@swc/counter/",\ @@ -5288,6 +5333,7 @@ const RAW_RUNTIME_STATE = ["@emotion/is-prop-valid", "npm:1.2.2"],\ ["@floating-ui/react-dom", "virtual:f29d69f85062a03ed7bd609b4d40e9adc04add82ffc06c75c0a45531f39eb1379ffd22a0748bbd55406686765a1c241fd4e759a24923460124aed6e3dd6c4096#npm:1.3.0"],\ ["@floating-ui/react-dom-interactions", "virtual:f29d69f85062a03ed7bd609b4d40e9adc04add82ffc06c75c0a45531f39eb1379ffd22a0748bbd55406686765a1c241fd4e759a24923460124aed6e3dd6c4096#npm:0.10.3"],\ + ["@suspensive/react", "virtual:f29d69f85062a03ed7bd609b4d40e9adc04add82ffc06c75c0a45531f39eb1379ffd22a0748bbd55406686765a1c241fd4e759a24923460124aed6e3dd6c4096#npm:2.10.0"],\ ["@tanstack/react-query", "virtual:f29d69f85062a03ed7bd609b4d40e9adc04add82ffc06c75c0a45531f39eb1379ffd22a0748bbd55406686765a1c241fd4e759a24923460124aed6e3dd6c4096#npm:5.50.1"],\ ["@tanstack/react-query-devtools", "virtual:f29d69f85062a03ed7bd609b4d40e9adc04add82ffc06c75c0a45531f39eb1379ffd22a0748bbd55406686765a1c241fd4e759a24923460124aed6e3dd6c4096#npm:5.32.1"],\ ["@toss/react", "virtual:f29d69f85062a03ed7bd609b4d40e9adc04add82ffc06c75c0a45531f39eb1379ffd22a0748bbd55406686765a1c241fd4e759a24923460124aed6e3dd6c4096#npm:1.6.1"],\ diff --git a/apps/next-app/package.json b/apps/next-app/package.json index 3a6649dc..b3f610b9 100644 --- a/apps/next-app/package.json +++ b/apps/next-app/package.json @@ -16,6 +16,7 @@ "@emotion/is-prop-valid": "^1.2.2", "@floating-ui/react-dom": "^1.0.0", "@floating-ui/react-dom-interactions": "^0.10.2", + "@suspensive/react": "^2.10.0", "@tanstack/react-query": "^5.50.1", "@tanstack/react-query-devtools": "^5.32.1", "@toss/react": "^1.3.6", diff --git a/apps/next-app/src/app/(hasGNB)/_components/Nav/Nav.tsx b/apps/next-app/src/app/(hasGNB)/_components/Nav/Nav.tsx index 3016b04b..bcc890ee 100644 --- a/apps/next-app/src/app/(hasGNB)/_components/Nav/Nav.tsx +++ b/apps/next-app/src/app/(hasGNB)/_components/Nav/Nav.tsx @@ -1,35 +1,32 @@ 'use client' -import { usePathname, useRouter } from 'next/navigation' +import { ErrorBoundary } from '@suspensive/react' +import { useRouter } from 'next/navigation' import { forwardRef, Suspense } from 'react' import * as S from 'components/common/Layout/Nav/Nav.style' import { Profile } from 'components/common/Layout/Nav/Profile' import { ROUTE } from 'constants/route' -import { getAccessTokenFromCookie } from 'features/auth/token' import { LogoButton } from './LogoButton' -const enableGoToSignUpButton = (pathname: string | null) => - pathname === ROUTE.SIGN_UP || pathname === ROUTE.INTRODUCE - // NOTE: 서버 컴포넌트로 만들면 클라 측에서 갱신이 안됨 +// app router용 const Nav = forwardRef(function Nav(props, ref) { - const pathname = usePathname() const router = useRouter() return ( - {enableGoToSignUpButton(pathname) ? ( - router.push(ROUTE.SIGN_UP)}> - 피둥 시작하기 - - ) : ( - getAccessTokenFromCookie() && ( - - - - ) - )} + router.push(ROUTE.SIGN_UP)}> + 피둥 시작하기 + + } + > + + + + ) }) diff --git a/apps/next-app/src/components/common/Layout/Nav/Nav.tsx b/apps/next-app/src/components/common/Layout/Nav/Nav.tsx index 0797be05..1e1d30b7 100644 --- a/apps/next-app/src/components/common/Layout/Nav/Nav.tsx +++ b/apps/next-app/src/components/common/Layout/Nav/Nav.tsx @@ -1,21 +1,18 @@ 'use client' -import { usePathname, useRouter } from 'next/navigation' -import { forwardRef, Suspense } from 'react' +import { ErrorBoundary, Suspense } from '@suspensive/react' +import { useRouter } from 'next/navigation' +import { forwardRef } from 'react' import LogoDesktopNoBackground from 'components/common/LogoDesktop' import { ROUTE } from 'constants/route' -import { getAccessTokenFromCookie } from 'features/auth/token' import { Profile } from './Profile' import * as S from './Nav.style' -const enableGoToSignUpButton = (pathname: string | null) => - pathname === ROUTE.SIGN_UP || pathname === ROUTE.INTRODUCE - +// pages router용 const Nav = forwardRef(function TopNavBar(props, ref) { const router = useRouter() - const pathname = usePathname() return ( @@ -23,17 +20,17 @@ const Nav = forwardRef(function TopNavBar(props, ref) { Feedoong - {enableGoToSignUpButton(pathname) ? ( - router.push(ROUTE.SIGN_UP)}> - 피둥 시작하기 - - ) : ( - getAccessTokenFromCookie() && ( - - - - ) - )} + router.push(ROUTE.SIGN_UP)}> + 피둥 시작하기 + + } + > + + + + ) }) diff --git a/apps/next-app/src/components/common/Layout/Nav/Profile.tsx b/apps/next-app/src/components/common/Layout/Nav/Profile.tsx index 655e7070..d0853adf 100644 --- a/apps/next-app/src/components/common/Layout/Nav/Profile.tsx +++ b/apps/next-app/src/components/common/Layout/Nav/Profile.tsx @@ -11,6 +11,8 @@ export const Profile = () => { const { data: userProfile } = useSuspenseQuery({ queryKey: CACHE_KEYS.me, queryFn: getUserInfoUsingGET, + retry: false, + meta: { ignoreToast: true }, }) return ( diff --git a/apps/next-app/src/components/common/Providers/index.tsx b/apps/next-app/src/components/common/Providers/index.tsx index b182483c..00945c4b 100644 --- a/apps/next-app/src/components/common/Providers/index.tsx +++ b/apps/next-app/src/components/common/Providers/index.tsx @@ -26,7 +26,8 @@ const Providers = ({ pageProps, children }: Props) => { }, }, queryCache: new QueryCache({ - onError: (err: unknown) => globalQueryErrorHandler(err, queryClient), + onError: (err: unknown, query) => + globalQueryErrorHandler(err, query, queryClient), }), }) ) diff --git a/apps/next-app/src/constants/route.ts b/apps/next-app/src/constants/route.ts index eacf065b..5f9f0624 100644 --- a/apps/next-app/src/constants/route.ts +++ b/apps/next-app/src/constants/route.ts @@ -8,5 +8,11 @@ export const FEED_ROUTE = { export const ROUTE = { INTRODUCE: '/introduce', SIGN_UP: '/signup', + MY_ACCOUNT: '/mypage/account', ...FEED_ROUTE, } as const + +export const PRIVATE_ROUTE = { + MY_FEED: ROUTE.MY_FEED, + MY_ACCOUNT: ROUTE.MY_ACCOUNT, +} diff --git a/apps/next-app/src/features/auth/token.ts b/apps/next-app/src/features/auth/token.ts index 660c76a4..0d5cf522 100644 --- a/apps/next-app/src/features/auth/token.ts +++ b/apps/next-app/src/features/auth/token.ts @@ -1,12 +1,37 @@ import dayjs from 'dayjs' import Cookies from 'js-cookie' +import { parseCookies } from 'nookies' import { AccessToken, RefreshToken } from 'constants/auth' import { isServer } from 'utils' import { getNextCookies, isAppRouter } from 'shared/libs/nextjs' +import { asyncLocalStorage } from 'shared/libs/context' -const getIsomorphicCookies = () => - isServer() && isAppRouter() ? getNextCookies() : Cookies +const getCookieAtPagesRouter = () => { + const store = asyncLocalStorage.getStore() + if (store) { + const cookie = parseCookies({ req: store.req }) + if (cookie) { + return { + get: (key: string) => cookie[key], + } + } + } + return { + get: () => void 0, + } +} + +export const getIsomorphicCookies = () => { + if (isServer()) { + if (isAppRouter()) { + return getNextCookies() + } else { + return getCookieAtPagesRouter() + } + } + return Cookies +} const getIsomorphicToken = (token: ReturnType) => { if (token instanceof Object && 'value' in token) { diff --git a/apps/next-app/src/features/auth/withAuthQueryServerSideProps.ts b/apps/next-app/src/features/auth/withAuthQueryServerSideProps.ts index db8f7e78..ca3a09ee 100644 --- a/apps/next-app/src/features/auth/withAuthQueryServerSideProps.ts +++ b/apps/next-app/src/features/auth/withAuthQueryServerSideProps.ts @@ -1,16 +1,57 @@ import { dehydrate, QueryClient } from '@tanstack/react-query' import type { GetServerSideProps, GetServerSidePropsContext } from 'next' -import type { feedoongApi } from 'services/api' import type { UserProfile } from 'services/auth' import { CACHE_KEYS } from 'services/cacheKeys' import { getUserInfoUsingGET } from 'services/types/_generated/user' +import { asyncLocalStorage } from 'shared/libs/context' +import { isServer } from 'utils' +import { getAccessTokenFromCookie } from './token' -export type GetServerSidePropsContextWithAuthClient = - GetServerSidePropsContext & { - queryClient: QueryClient - api: ReturnType +export type GetServerSidePropsContextWithAuthClient = GetServerSidePropsContext + +// 서버 사이드 렌더링 시 컨텍스트를 설정하는 미들웨어 +export function withRequestContext( + handler: (context: GetServerSidePropsContext) => Promise +) { + return async (context: GetServerSidePropsContext) => { + if (isServer()) { + return asyncLocalStorage.run({ req: context.req }, () => handler(context)) + } + return handler(context) + } +} + +// NOTE: prefetch 로직을 서버 컴포넌트에서 좀 더 적절히 구현하기 +export const withPrefetchUser = withRequestContext(async () => { + try { + if (!getAccessTokenFromCookie()) { + return { + props: {}, + } + } + + const queryClient = new QueryClient() + + await queryClient.prefetchQuery({ + queryKey: CACHE_KEYS.me, + queryFn: getUserInfoUsingGET, + }) + + const dehydratedState = JSON.parse(JSON.stringify(dehydrate(queryClient))) + + return { + props: { + dehydratedState, + }, + } + } catch (error) { + console.log(error) + return { + props: {}, + } } +}) export const withAuthQueryServerSideProps = ( getServerSidePropsFunc?: GetServerSideProps @@ -18,7 +59,6 @@ export const withAuthQueryServerSideProps = ( return async (context: GetServerSidePropsContextWithAuthClient) => { try { const queryClient = new QueryClient() - context.queryClient = queryClient await queryClient.prefetchQuery({ queryKey: CACHE_KEYS.me, @@ -42,7 +82,7 @@ export const withAuthQueryServerSideProps = ( } const dehydratedState = JSON.parse( - JSON.stringify(dehydrate(context.queryClient)) + JSON.stringify(dehydrate(queryClient)) ) return { diff --git a/apps/next-app/src/features/errors/globalQueryErrorHandler.ts b/apps/next-app/src/features/errors/globalQueryErrorHandler.ts index 85885fee..dc3d804c 100644 --- a/apps/next-app/src/features/errors/globalQueryErrorHandler.ts +++ b/apps/next-app/src/features/errors/globalQueryErrorHandler.ts @@ -1,8 +1,8 @@ -import type { QueryClient } from '@tanstack/react-query' +import type { Query, QueryClient } from '@tanstack/react-query' import { AxiosError } from 'axios' import Toast from 'components/common/Toast' -import { ROUTE } from 'constants/route' +import { PRIVATE_ROUTE, ROUTE } from 'constants/route' import { CACHE_KEYS } from 'services/cacheKeys' import { RESPONSE_CODE } from 'types/common' import { isServer } from 'utils' @@ -10,6 +10,7 @@ import { destroyTokensClientSide } from 'utils/auth' export const globalQueryErrorHandler = ( err: unknown, + query: Query, queryClient: QueryClient ) => { if (err instanceof AxiosError) { @@ -19,12 +20,19 @@ export const globalQueryErrorHandler = ( destroyTokensClientSide() queryClient.invalidateQueries({ queryKey: CACHE_KEYS.me }) } - goToIntroducePage() + const isClient = !isServer() + const ignoreToast = query.meta?.ignoreToast - Toast.show({ - type: 'error', - content: err.response?.data.message ?? '에러가 발생했습니다.', - }) + if (isClient && !ignoreToast) { + Toast.show({ + type: 'error', + content: err.response?.data.message ?? '에러가 발생했습니다.', + }) + } + + if (isClient && isPrivatePath()) { + goToIntroducePage() + } } } @@ -35,6 +43,11 @@ const goToIntroducePage = () => { } } +const isPrivatePath = () => + Object.values(PRIVATE_ROUTE).find((path) => + window.location.pathname.includes(path) + ) + const isDestroyTokenError = (code: string) => [ RESPONSE_CODE.REFRESH_TOKEN_NOT_FOUND, diff --git a/apps/next-app/src/pages/[userName].tsx b/apps/next-app/src/pages/[userName].tsx index 52ab50fd..f2d63543 100644 --- a/apps/next-app/src/pages/[userName].tsx +++ b/apps/next-app/src/pages/[userName].tsx @@ -1,9 +1,12 @@ import type { NextPage } from 'next' import UserPageContainer from 'components/views/UserPage' +import { withPrefetchUser } from 'features/auth/withAuthQueryServerSideProps' const UserProfile: NextPage = () => { return } export default UserProfile + +export const getServerSideProps = withPrefetchUser diff --git a/apps/next-app/src/pages/channels/[id].tsx b/apps/next-app/src/pages/channels/[id].tsx index 99cc397b..906b5f7a 100644 --- a/apps/next-app/src/pages/channels/[id].tsx +++ b/apps/next-app/src/pages/channels/[id].tsx @@ -1,9 +1,12 @@ import type { NextPage } from 'next' import ChannelDetailView from 'components/views/Channel/ChannelDetailContainer' +import { withPrefetchUser } from 'features/auth/withAuthQueryServerSideProps' const ChannelDetail: NextPage = () => { return } export default ChannelDetail + +export const getServerSideProps = withPrefetchUser diff --git a/apps/next-app/src/pages/feed/recommended/channels/index.tsx b/apps/next-app/src/pages/feed/recommended/channels/index.tsx index 09456c86..c962b9bc 100644 --- a/apps/next-app/src/pages/feed/recommended/channels/index.tsx +++ b/apps/next-app/src/pages/feed/recommended/channels/index.tsx @@ -1,8 +1,9 @@ import type { NextPage } from 'next' import Head from 'next/head' -import RssInputView from 'components/views/RssInput' import FeedsContainerView from 'components/views/Feeds/FeedsContainer' +import RssInputView from 'components/views/RssInput' +import { withPrefetchUser } from 'features/auth/withAuthQueryServerSideProps' import { useGetUserProfile } from 'features/user/userProfile' const Home: NextPage = () => { @@ -20,3 +21,5 @@ const Home: NextPage = () => { } export default Home + +export const getServerSideProps = withPrefetchUser diff --git a/apps/next-app/src/pages/feed/recommended/posts/index.tsx b/apps/next-app/src/pages/feed/recommended/posts/index.tsx index 653d0886..41aee214 100644 --- a/apps/next-app/src/pages/feed/recommended/posts/index.tsx +++ b/apps/next-app/src/pages/feed/recommended/posts/index.tsx @@ -3,7 +3,10 @@ import Head from 'next/head' import RssInputView from 'components/views/RssInput' import FeedsContainerView from 'components/views/Feeds/FeedsContainer' -import { withAuthQueryServerSideProps } from 'features/auth/withAuthQueryServerSideProps' +import { + withAuthQueryServerSideProps, + withRequestContext, +} from 'features/auth/withAuthQueryServerSideProps' import { useGetUserProfile } from 'features/user/userProfile' const Home: NextPage = () => { @@ -22,4 +25,6 @@ const Home: NextPage = () => { export default Home -export const getServerSideProps = withAuthQueryServerSideProps() +export const getServerSideProps = withRequestContext( + withAuthQueryServerSideProps() +) diff --git a/apps/next-app/src/shared/libs/context.ts b/apps/next-app/src/shared/libs/context.ts new file mode 100644 index 00000000..30d3545b --- /dev/null +++ b/apps/next-app/src/shared/libs/context.ts @@ -0,0 +1,10 @@ +import type { IncomingMessage } from 'http' + +export let asyncLocalStorage: any +const isServer = typeof window === 'undefined' + +if (isServer) { + asyncLocalStorage = new AsyncLocalStorage<{ + req: IncomingMessage + }>() +} diff --git a/yarn.lock b/yarn.lock index ac99a665..2f4aa7e2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1176,6 +1176,26 @@ __metadata: languageName: node linkType: hard +"@suspensive/react@npm:^2.10.0": + version: 2.10.0 + resolution: "@suspensive/react@npm:2.10.0" + dependencies: + "@suspensive/utils": "npm:2.10.0" + peerDependencies: + react: ^18 + checksum: 10c0/37d28394298a66ded5afe25b926304d77525cd1c13e2ff31ab50313a43c57fa3b763625bd5dc31e5168f17b4f7d72ea0b57022ed996543e1de2ec5532b324479 + languageName: node + linkType: hard + +"@suspensive/utils@npm:2.10.0": + version: 2.10.0 + resolution: "@suspensive/utils@npm:2.10.0" + peerDependencies: + react: ^18 + checksum: 10c0/5589c1b12e5059cb2af98769d5ad57ee1011369fc308ba7b588aa75e22aa3ca5cd1130fd83b2c52632ffd856d9a9c2c99f98c8baf3a52ee99067266179b53c59 + languageName: node + linkType: hard + "@swc/counter@npm:^0.1.3": version: 0.1.3 resolution: "@swc/counter@npm:0.1.3" @@ -4042,6 +4062,7 @@ __metadata: "@emotion/is-prop-valid": "npm:^1.2.2" "@floating-ui/react-dom": "npm:^1.0.0" "@floating-ui/react-dom-interactions": "npm:^0.10.2" + "@suspensive/react": "npm:^2.10.0" "@tanstack/react-query": "npm:^5.50.1" "@tanstack/react-query-devtools": "npm:^5.32.1" "@toss/react": "npm:^1.3.6"