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 516cae0c..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,60 +1,32 @@ -import React, { forwardRef } from 'react' -import { cookies } from 'next/headers' +'use client' +import { ErrorBoundary } from '@suspensive/react' +import { useRouter } from 'next/navigation' +import { forwardRef, Suspense } 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 { GoToSignUpButton } from './GoToSignUpButton' +import { Profile } from 'components/common/Layout/Nav/Profile' +import { ROUTE } from 'constants/route' 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() -} - -const Nav = forwardRef(async function Nav(props, ref) { - const userProfile = await getUserProfile() +// NOTE: 서버 컴포넌트로 만들면 클라 측에서 갱신이 안됨 +// app router용 +const Nav = forwardRef(function Nav(props, ref) { + const router = useRouter() return ( - - {userProfile?.name ? ( - - - {`${userProfile.name}님, 안녕하세요!`} - {userProfile.profileImageUrl && ( - - )} - - - ) : ( - - )} + router.push(ROUTE.SIGN_UP)}> + 피둥 시작하기 + + } + > + + + + ) }) 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..708af2b0 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' - -const FeedMePage: NextPage = () => { - const api = feedoongApi() - const cookieStore = cookies() - const accessToken = `${cookieStore.get('accessToken')?.value}` - - setAuthorizationHeader(api, accessToken, { type: 'Bearer' }) +import MyFeed from 'views/myFeed' +const FeedMePage: NextPage = async () => { const queryClient = getQueryClient() - void queryClient.prefetchInfiniteQuery(itemQueries.list(api)) + await queryClient.prefetchInfiniteQuery(itemQueries.list()) return ( @@ -28,7 +20,7 @@ const FeedMePage: NextPage = () => { ))} > - + ) 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` 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/common/Layout/Nav/Nav.tsx b/apps/next-app/src/components/common/Layout/Nav/Nav.tsx index 643de00d..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,18 +1,18 @@ 'use client' -import React, { forwardRef } from 'react' +import { ErrorBoundary, Suspense } from '@suspensive/react' import { useRouter } from 'next/navigation' +import { forwardRef } from 'react' -import { useGetUserProfile } from 'features/user/userProfile' -import ProfilePopover from './ProfilePopover' -import { ROUTE } from 'constants/route' import LogoDesktopNoBackground from 'components/common/LogoDesktop' +import { ROUTE } from 'constants/route' +import { Profile } from './Profile' import * as S from './Nav.style' +// pages router용 const Nav = forwardRef(function TopNavBar(props, ref) { const router = useRouter() - const { data: userProfile } = useGetUserProfile() return ( @@ -20,27 +20,17 @@ const Nav = forwardRef(function TopNavBar(props, ref) { Feedoong - - {userProfile?.name ? ( - - - {`${userProfile.name}님, 안녕하세요!`} - {userProfile.profileImageUrl && ( - - )} - - - ) : ( - router.push(ROUTE.SIGN_UP)}> - 피둥 시작하기 - - )} + 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 new file mode 100644 index 00000000..d0853adf --- /dev/null +++ b/apps/next-app/src/components/common/Layout/Nav/Profile.tsx @@ -0,0 +1,34 @@ +'use client' +import { useSuspenseQuery } from '@tanstack/react-query' + +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, + retry: false, + meta: { ignoreToast: true }, + }) + + return ( + + + {`${userProfile.name}님, 안녕하세요!`} + {userProfile.profileImageUrl && ( + + )} + + + ) +} 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/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/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 e42ab3d9..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,19 +1,24 @@ +'use client' 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/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/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/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/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/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/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/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/entities/item/api/index.ts b/apps/next-app/src/entities/item/api/index.ts index 132ccdd5..513c6bc2 100644 --- a/apps/next-app/src/entities/item/api/index.ts +++ b/apps/next-app/src/entities/item/api/index.ts @@ -1,23 +1,21 @@ import { infiniteQueryOptions } from '@tanstack/react-query' -import type { AxiosInstance } from 'axios' -import { getFeeds, getFeedsServerSide } from 'services/feeds' -// import { getItemsUsingGET } from 'services/types/_generated/item' +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) + queryFn: ({ pageParam = 1 }) => { + return getItemsUsingGET({ + page: pageParam, + size: 10, + }) }, initialPageParam: 1, getNextPageParam: (lastPage) => { - return lastPage.items.length === 10 ? lastPage.next : undefined + return lastPage.next }, }), } 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/auth/token.ts b/apps/next-app/src/features/auth/token.ts index 806c7178..0d5cf522 100644 --- a/apps/next-app/src/features/auth/token.ts +++ b/apps/next-app/src/features/auth/token.ts @@ -1,40 +1,74 @@ -import Cookies from 'js-cookie' -import type { AxiosInstance } from 'axios' 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 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) { + return token.value + } + return token +} export const getRefreshTokenFromCookie = () => { - return Cookies.get(RefreshToken) + const cookies = getIsomorphicCookies() + const token = getIsomorphicToken(cookies.get(RefreshToken)) + + return token } export const setRefreshTokenToCookie = (token: string) => { - Cookies.set(RefreshToken, token, { + const cookies = getIsomorphicCookies() + + cookies.set(RefreshToken, token, { expires: dayjs().add(6, 'month').toDate(), sameSite: 'lax', }) } export const getAccessTokenFromCookie = () => { - return Cookies.get(AccessToken) + const cookies = getIsomorphicCookies() + const token = getIsomorphicToken(cookies.get(AccessToken)) + + return token } export const setAccessTokenToCookie = (token: string) => { - Cookies.set(AccessToken, token, { + const cookies = getIsomorphicCookies() + + cookies.set(AccessToken, token, { expires: dayjs().add(1, 'month').toDate(), secure: true, 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..ca3a09ee 100644 --- a/apps/next-app/src/features/auth/withAuthQueryServerSideProps.ts +++ b/apps/next-app/src/features/auth/withAuthQueryServerSideProps.ts @@ -1,39 +1,68 @@ -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 { UserProfile } from 'services/auth' -import { getUserInfoServerSide } 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' +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 + +// 서버 사이드 렌더링 시 컨텍스트를 설정하는 미들웨어 +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))) -export type GetServerSidePropsContextWithAuthClient = - GetServerSidePropsContext & { - queryClient: QueryClient - api: ReturnType + return { + props: { + dehydratedState, + }, + } + } catch (error) { + console.log(error) + return { + props: {}, + } } +}) export const withAuthQueryServerSideProps = ( getServerSidePropsFunc?: GetServerSideProps ) => { 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: getUserInfoUsingGET, }) if (!getServerSidePropsFunc) { @@ -53,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/channel/index.ts b/apps/next-app/src/features/channel/index.ts index d68ab1f3..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 { submitRssUrl } from 'services/feeds' -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' @@ -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: '채널을 등록중이에요', @@ -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/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/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/features/user/userProfile.ts b/apps/next-app/src/features/user/userProfile.ts index a3907c73..f0389337 100644 --- a/apps/next-app/src/features/user/userProfile.ts +++ b/apps/next-app/src/features/user/userProfile.ts @@ -3,15 +3,20 @@ 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, + // NOTE: 왜 액세스 토큰이 아니라 리프레시 토큰? enabled: !!getRefreshTokenFromCookie(), ...options, }) @@ -19,11 +24,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: () => getPublicUserInfoUsingGET(username), ...options, enabled: !!username, }) 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/pages/oauth/index.tsx b/apps/next-app/src/pages/oauth/index.tsx index f7f8e2a4..e6844df5 100644 --- a/apps/next-app/src/pages/oauth/index.tsx +++ b/apps/next-app/src/pages/oauth/index.tsx @@ -4,12 +4,10 @@ import qs from 'query-string' import humps from 'humps' import { useEffect } from 'react' -import { submitAccessToken } from 'services/auth' -import api from 'services/api' +import { loginUsingPOST } from 'services/types/_generated/user' import { CACHE_KEYS } from 'services/cacheKeys' import { setAccessTokenToCookie, - setAuthorizationHeader, setRefreshTokenToCookie, } from 'features/auth/token' @@ -19,14 +17,14 @@ const Oauth = () => { const { data, isError } = useQuery({ queryKey: CACHE_KEYS.signup, - queryFn: () => submitAccessToken(parseAccessToken(router.asPath)), + queryFn: () => + loginUsingPOST({ accessToken: parseAccessToken(router.asPath) }), }) useEffect(() => { 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 deleted file mode 100644 index 401f9466..00000000 --- a/apps/next-app/src/services/account/index.ts +++ /dev/null @@ -1,5 +0,0 @@ -import api from 'services/api' - -export const deleteAccount = () => { - return api.delete('/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..c07600a0 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,18 +12,20 @@ import { const { camelizeKeys } = humps -export const feedoongApi = () => { +export const feedoongApi = (config: AxiosRequestConfig): Promise => { const accessToken = getAccessTokenFromCookie() const _api = Axios.create({ baseURL: getApiEndpoint(), validateStatus: (status) => status >= httpStatus.OK && status < httpStatus.BAD_REQUEST, // 200 ~ 399 - headers: { - ...(accessToken && { Authorization: `Bearer ${accessToken}` }), - }, }) + config.headers = { + ...config.headers, + Authorization: accessToken ? `Bearer ${accessToken}` : undefined, + } + _api.interceptors.response.use( // try (response) => { @@ -54,9 +56,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..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 api from 'services/api' import { getRefreshTokenFromCookie, setAccessTokenToCookie, - setAuthorizationHeader, setRefreshTokenToCookie, } from 'features/auth/token' +import type UserProfile from 'pages/[userName]' +import { reissueTokenUsingPOST } from 'services/types/_generated/user' export interface UserProfile { email: string @@ -26,53 +25,25 @@ export interface SignUpResponse extends UserProfile { refreshToken: string } -export const submitAccessToken = (token: string) => { - return api.post(`/users/login/google`, null, { - params: { accessToken: token }, - }) -} - -export const getUserInfo = () => { - return api.get(`/users/me`) -} - -export const getUserInfoByUsername = (username: string) => { - return api.get>( - `/users/${username}/info` - ) -} - -export const getUserInfoServerSide = (_api: AxiosInstance) => () => { - return _api.get(`/users/me`) -} - 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) - - setAuthorizationHeader(_api, newAccessToken, { type: 'Bearer' }) + 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/feeds/index.ts b/apps/next-app/src/services/feeds/index.ts deleted file mode 100644 index 76e1b7cd..00000000 --- a/apps/next-app/src/services/feeds/index.ts +++ /dev/null @@ -1,91 +0,0 @@ -import type { AxiosInstance } from 'axios' - -import api from 'services/api' -import type { - Feed, - LikePostResponse, - PreviewResponse, - SubmitRssUrlParams, - SubmitRssUrlResponse, - SubmitViewedPost, -} from 'types/feeds' - -export const getFeeds = (page = 1, size = 10) => { - return api.get(`/items`, { - params: { - page, - size, - }, - }) -} - -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}`, { - params: { - page, - size, - }, - }) -} - -export const checkUrlAsRss = (url: string) => { - return api.get(`/channels/preview`, { - params: { url }, - }) -} - -export const checkUrlAsDirectRss = ({ - homeUrl, - rssFeedUrl, -}: { - homeUrl: string - rssFeedUrl: string -}) => { - return api.get(`/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 api.post(`/channels`, { - ...params, - }) -} - -export const likePost = (id: string) => { - return api.post(`/likes/${id}`) -} - -export const unlikePost = (id: string) => { - return api.delete(`/likes/${id}`) -} - -export const getLikedPosts = (page: number) => { - return api.get(`/items/liked`, { - params: { page }, - }) -} - -export const submitViewedPost = (id: number) => { - return api.post(`/items/view/${id}`) -} - -export const getLikedPostsByUsername = (page: number, username?: string) => { - return api.get(`/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 deleted file mode 100644 index 07758ee8..00000000 --- a/apps/next-app/src/services/recommendations/index.ts +++ /dev/null @@ -1,23 +0,0 @@ -import type { AxiosInstance } from 'axios' - -import api from 'services/api' -import type { Post } from 'types/feeds' -import type { Channel } from 'types/subscriptions' - -export const getRecommendedChannels = () => { - return api.get(`/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`) -} 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 8f2aeda7..00000000 --- a/apps/next-app/src/services/subscriptions/index.ts +++ /dev/null @@ -1,18 +0,0 @@ -import api from 'services/api' -import type { Channels } from 'types/subscriptions' - -export const getChannels = (page: number) => { - return api.get(`/subscriptions`, { - params: { page }, - }) -} - -export const deleteChannel = (channelId: number) => { - return api.delete(`/subscriptions/${channelId}`) -} - -export const getChannelsByUsername = (page: number, username?: string) => { - return api.get(`/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/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/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() 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 } diff --git a/apps/next-app/src/views/myFeed/MyFeed.tsx b/apps/next-app/src/views/myFeed/MyFeed.tsx index e4cdb5e4..b4f4d8b1 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 { 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' @@ -14,12 +14,8 @@ interface Props { } const MyFeed = ({ isLoggedIn }: Props) => { - const { - data: itemList, - fetchNextPage, - isFetchingNextPage, - hasNextPage, - } = useSuspenseInfiniteQuery(itemQueries.list()) + const { data, fetchNextPage, isFetchingNextPage, hasNextPage } = + useSuspenseInfiniteQuery(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) => ( )) 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"