diff --git a/next.config.js b/next.config.js index fa48f8a5..e5d052a9 100644 --- a/next.config.js +++ b/next.config.js @@ -12,8 +12,19 @@ const withBundleAnalyzer = require('@next/bundle-analyzer')({ }) const nextConfig = { + reactStrictMode: false, images: { - domains: ['linkhub-s3.s3.ap-northeast-2.amazonaws.com'], + minimumCacheTTL: 1 * 60 * 60 * 24 * 365, + domains: ['linkhub-s3-2025.s3.ap-northeast-2.amazonaws.com'], + formats: ['image/avif', 'image/webp'], + remotePatterns: [ + { + protocol: 'https', + hostname: 'linkhub-s3-2025.s3.ap-northeast-2.amazonaws.com', + port: '', + pathname: '/**', + }, + ], }, async redirects() { return [ diff --git a/package-lock.json b/package-lock.json index a7a41c83..c1328fb0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -21,8 +21,8 @@ "next-pwa": "^5.6.0", "next-themes": "^0.2.1", "open-graph-scraper": "^6.3.2", - "react": "^18", - "react-dom": "^18", + "react": "^18.2.0", + "react-dom": "^18.2.0", "react-hook-form": "^7.47.0", "react-spinners": "^0.13.8", "react-toastify": "^9.1.3", @@ -3008,37 +3008,29 @@ } }, "node_modules/@tanstack/query-core": { - "version": "5.8.3", - "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.8.3.tgz", - "integrity": "sha512-SWFMFtcHfttLYif6pevnnMYnBvxKf3C+MHMH7bevyYfpXpTMsLB9O6nNGBdWSoPwnZRXFNyNeVZOw25Wmdasow==", + "version": "5.51.21", + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.51.21.tgz", + "integrity": "sha512-POQxm42IUp6n89kKWF4IZi18v3fxQWFRolvBA6phNVmA8psdfB1MvDnGacCJdS+EOX12w/CyHM62z//rHmYmvw==", + "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/tannerlinsley" } }, "node_modules/@tanstack/react-query": { - "version": "5.8.4", - "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.8.4.tgz", - "integrity": "sha512-CD+AkXzg8J72JrE6ocmuBEJfGzEzu/bzkD6sFXFDDB5yji9N20JofXZlN6n0+CaPJuIi+e4YLCbGsyPFKkfNQA==", + "version": "5.51.23", + "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.51.23.tgz", + "integrity": "sha512-CfJCfX45nnVIZjQBRYYtvVMIsGgWLKLYC4xcUiYEey671n1alvTZoCBaU9B85O8mF/tx9LPyrI04A6Bs2THv4A==", + "license": "MIT", "dependencies": { - "@tanstack/query-core": "5.8.3" + "@tanstack/query-core": "5.51.21" }, "funding": { "type": "github", "url": "https://github.com/sponsors/tannerlinsley" }, "peerDependencies": { - "react": "^18.0.0", - "react-dom": "^18.0.0", - "react-native": "*" - }, - "peerDependenciesMeta": { - "react-dom": { - "optional": true - }, - "react-native": { - "optional": true - } + "react": "^18.0.0" } }, "node_modules/@trivago/prettier-plugin-sort-imports": { diff --git a/package.json b/package.json index caf04a8f..99e3d9d0 100644 --- a/package.json +++ b/package.json @@ -26,8 +26,8 @@ "next-pwa": "^5.6.0", "next-themes": "^0.2.1", "open-graph-scraper": "^6.3.2", - "react": "^18", - "react-dom": "^18", + "react": "^18.2.0", + "react-dom": "^18.2.0", "react-hook-form": "^7.47.0", "react-spinners": "^0.13.8", "react-toastify": "^9.1.3", diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 0c82f8ec..9c596afc 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -1,3 +1,4 @@ +import { Suspense } from 'react' import { Providers } from '@/components' import Header from '@/components/common/Header/Header' import ToastContainer from '@/components/common/Toast/ToastContainer' @@ -35,7 +36,7 @@ export const metadata: Metadata = { locale: 'ko_KR', type: 'website', images: [ - 'https://linkhub-s3.s3.ap-northeast-2.amazonaws.com/linkhub-og-image.png', + 'https://linkhub-s3-2025.s3.ap-northeast-2.amazonaws.com/linkhub-og-image.png', ], }, manifest: '/manifest.json', @@ -53,17 +54,15 @@ export default function RootLayout({ children: React.ReactNode }) { return ( - + - + -
-
+
+ +
+
{children}
diff --git a/src/app/not-found.tsx b/src/app/not-found.tsx index 9ed7ea91..1360b4c7 100644 --- a/src/app/not-found.tsx +++ b/src/app/not-found.tsx @@ -11,6 +11,7 @@ const NotFoundPage = () => { width={240} height={300} alt="404" + placeholder="blur" />
{NOT_FOUND.TEXT_1}
diff --git a/src/app/page.tsx b/src/app/page.tsx index 051c365e..7d5fe810 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1,110 +1,22 @@ -'use client' - -import { CategoryList, Dropdown, LinkItem, Spinner } from '@/components' -import FloatingButton from '@/components/FloatingButton/FloatingButton' -import { ChipColors } from '@/components/common/Chip/Chip' -import DeferredComponent from '@/components/common/DeferedComponent/DeferedComponent' -import MainSpaceList from '@/components/common/MainSpaceList/MainSpaceList' -import { useCategoryParam, useSortParam } from '@/hooks' -import useGetPopularLinks from '@/hooks/useGetPopularLinks' -import { fetchGetSpaces } from '@/services/space/spaces' -import { PopularLinkResBody } from '@/types' -import 'swiper/css' -import 'swiper/css/free-mode' -import 'swiper/css/pagination' -import { FreeMode } from 'swiper/modules' -import { Swiper, SwiperSlide } from 'swiper/react' +import HydratePopularLinkList from '@/components/PopularLinkList/HydratePopularLinkList' +import FloatingBtnController from '@/components/common/MainSpaceList/FloatingBtnController' +import MainSpaceHeader from '@/components/common/MainSpaceList/MainSpaceHeader' +import SpaceListController from '@/components/common/MainSpaceList/SpaceListController' export default function Home() { - const { links, isPopularLinksLoading } = useGetPopularLinks() - const { sort, sortIndex, handleSortChange } = useSortParam('space') - const { category, categoryIndex, handleCategoryChange } = - useCategoryParam('all') - - return isPopularLinksLoading ? ( - - - - ) : ( + return ( <>

인기있는 링크

- {links && ( - - {links.map((link: PopularLinkResBody) => ( - - - - ))} - - )} +
+ +
-
-
-

스페이스 모음

- -
- -
- + +
- + ) } diff --git a/src/components/PopularLink/PopularLinkList/PopularLinkList.tsx b/src/components/PopularLink/PopularLinkList/PopularLinkList.tsx new file mode 100644 index 00000000..42fb2768 --- /dev/null +++ b/src/components/PopularLink/PopularLinkList/PopularLinkList.tsx @@ -0,0 +1,66 @@ +'use client' + +import useGetPopularLinks from '@/components/PopularLinkList/hooks/useGetPopularLinks' +import { ChipColors } from '@/components/common/Chip/Chip' +import LinkItem from '@/components/common/LinkItem/LinkItem' +import { PopularLinkResBody } from '@/types' +import 'swiper/css' +import 'swiper/css/free-mode' +import 'swiper/css/pagination' +import { FreeMode } from 'swiper/modules' +import { Swiper, SwiperSlide } from 'swiper/react' + +const PopularLinkList = () => { + const { data } = useGetPopularLinks() + return ( + + {data?.responses.map((link: PopularLinkResBody) => ( + + + + ))} + + ) +} + +export default PopularLinkList diff --git a/src/components/PopularLinkList/HydratePopularLinkList.tsx b/src/components/PopularLinkList/HydratePopularLinkList.tsx new file mode 100644 index 00000000..659e7037 --- /dev/null +++ b/src/components/PopularLinkList/HydratePopularLinkList.tsx @@ -0,0 +1,21 @@ +import React from 'react' +import { getQueryClient } from '@/lib/queryClient' +import { fetchGetPopularLinks } from '@/services/link/link' +import { HydrationBoundary, dehydrate } from '@tanstack/react-query' +import PopularLinkList from './PopularLinkList' + +const HydratePopularLinkList = async () => { + const queryClient = getQueryClient() + await queryClient.prefetchQuery({ + queryKey: ['PopularLinks'], + queryFn: fetchGetPopularLinks, + }) + + return ( + + + + ) +} + +export default React.memo(HydratePopularLinkList) diff --git a/src/components/PopularLinkList/PopularLinkList.tsx b/src/components/PopularLinkList/PopularLinkList.tsx new file mode 100644 index 00000000..9c0ee71f --- /dev/null +++ b/src/components/PopularLinkList/PopularLinkList.tsx @@ -0,0 +1,68 @@ +'use client' + +import React from 'react' +import { ChipColors } from '@/components/common/Chip/Chip' +import LinkItem from '@/components/common/LinkItem/LinkItem' +import { PopularLinkResBody } from '@/types' +import 'swiper/css' +import 'swiper/css/free-mode' +import 'swiper/css/pagination' +import { FreeMode } from 'swiper/modules' +import { Swiper, SwiperSlide } from 'swiper/react' +import useGetPopularLinks from './hooks/useGetPopularLinks' + +const PopularLinkList = () => { + const { data } = useGetPopularLinks() + + return ( + + {data?.responses.map((link: PopularLinkResBody) => ( + + + + ))} + + ) +} + +export default React.memo(PopularLinkList) diff --git a/src/components/PopularLinkList/PopularLinkSkeleton.tsx b/src/components/PopularLinkList/PopularLinkSkeleton.tsx new file mode 100644 index 00000000..5251b030 --- /dev/null +++ b/src/components/PopularLinkList/PopularLinkSkeleton.tsx @@ -0,0 +1,36 @@ +const LinkSkeleton = () => { + return ( +
+
+
+
+
+
+
+
+
+
+
+ ) +} + +const PopularLinkSkeleton = () => { + return ( +
+
+ + + + + + + + + + +
+
+ ) +} + +export default PopularLinkSkeleton diff --git a/src/components/PopularLinkList/hooks/useGetPopularLinks.ts b/src/components/PopularLinkList/hooks/useGetPopularLinks.ts new file mode 100644 index 00000000..ffaf58f5 --- /dev/null +++ b/src/components/PopularLinkList/hooks/useGetPopularLinks.ts @@ -0,0 +1,11 @@ +import { fetchGetPopularLinks } from '@/services/link/link' +import { useQuery } from '@tanstack/react-query' + +const useGetPopularLinks = () => { + return useQuery({ + queryKey: ['PopularLinks'], + queryFn: fetchGetPopularLinks, + }) +} + +export default useGetPopularLinks diff --git a/src/components/Providers/Providers.tsx b/src/components/Providers/Providers.tsx index 2e08beaf..6e228838 100644 --- a/src/components/Providers/Providers.tsx +++ b/src/components/Providers/Providers.tsx @@ -1,20 +1,9 @@ 'use client' -import { useEffect, useState } from 'react' -import { ThemeProvider } from 'next-themes' +import { ThemeProvider as NextThemesProvider } from 'next-themes' const Providers = ({ children }: { children: React.ReactNode }) => { - const [isMount, setMount] = useState(false) - - useEffect(() => { - setMount(true) - }, []) - - if (!isMount) { - return null - } - - return {children} + return {children} } export default Providers diff --git a/src/components/Space/SpaceForm.tsx b/src/components/Space/SpaceForm.tsx index c20e9a84..9ac3204b 100644 --- a/src/components/Space/SpaceForm.tsx +++ b/src/components/Space/SpaceForm.tsx @@ -114,6 +114,7 @@ const SpaceForm = ({ spaceType, space }: SpaceFormProps) => {
selectSpaceImage?.current?.click()}> {thumnail ? ( { const sortValue = - queryKey === 'main' || queryKey === 'search' + queryKey === 'main' ? sort === 'favorite' ? 'favorite_count' : 'created_at' @@ -48,7 +51,10 @@ const useMainSpacesQuery = ({ filter: categoryValue, keyWord: keyword, }), - initialPageParam: { lastSpaceId: undefined, lastFavoriteCount: undefined }, + initialPageParam: { + lastSpaceId: undefined, + lastFavoriteCount: undefined, + }, getNextPageParam: ( lastPage: MainSpacePageType, ): diff --git a/src/components/common/Avatar/Avatar.tsx b/src/components/common/Avatar/Avatar.tsx index b244c81b..12a37789 100644 --- a/src/components/common/Avatar/Avatar.tsx +++ b/src/components/common/Avatar/Avatar.tsx @@ -14,6 +14,7 @@ export interface AvatarProps { const Avatar = ({ src, alt, className }: AvatarProps) => { return ( {alt} { + const { categoryIndex, handleCategoryChange } = useCategoryParam('all') + + return ( + + ) +} + +export default CategoryListController diff --git a/src/components/common/CategoryList/CategoryListItem.tsx b/src/components/common/CategoryList/CategoryListItem.tsx index d40c7a5a..db440ca5 100644 --- a/src/components/common/CategoryList/CategoryListItem.tsx +++ b/src/components/common/CategoryList/CategoryListItem.tsx @@ -30,6 +30,7 @@ const CategoryListItem = ({ return as === 'link' ? (
{nickname} diff --git a/src/components/common/Dropdown/DropdownController.tsx b/src/components/common/Dropdown/DropdownController.tsx new file mode 100644 index 00000000..d8424cfc --- /dev/null +++ b/src/components/common/Dropdown/DropdownController.tsx @@ -0,0 +1,19 @@ +'use client' + +import { useSortParam } from '@/hooks' +import Dropdown from './Dropdown' + +const DropdownController = () => { + const { sortIndex, handleSortChange } = useSortParam('space') + + return ( + + ) +} + +export default DropdownController diff --git a/src/components/common/Header/Header.tsx b/src/components/common/Header/Header.tsx index fcdc1246..e003989a 100644 --- a/src/components/common/Header/Header.tsx +++ b/src/components/common/Header/Header.tsx @@ -31,7 +31,9 @@ const Header = () => {
@@ -44,6 +46,7 @@ const Header = () => {
{isLoggedIn && ( diff --git a/src/components/common/LinkItem/LinkItem.tsx b/src/components/common/LinkItem/LinkItem.tsx index 3f181839..237f802e 100644 --- a/src/components/common/LinkItem/LinkItem.tsx +++ b/src/components/common/LinkItem/LinkItem.tsx @@ -124,6 +124,7 @@ const LinkItem = ({ {type === 'list' ? (
isMember && handleSaveReadInfo({ spaceId, linkId })} className="cursor-pointer overflow-hidden text-ellipsis whitespace-nowrap text-sm font-medium text-gray9" href={url} @@ -199,6 +200,7 @@ const LinkItem = ({ ) : (
isMember && handleSaveReadInfo({ spaceId, linkId })} className="cursor-pointer overflow-hidden text-ellipsis whitespace-nowrap text-sm font-medium text-gray9" href={url} diff --git a/src/components/common/MainSpaceList/FloatingBtnController.tsx b/src/components/common/MainSpaceList/FloatingBtnController.tsx new file mode 100644 index 00000000..577f5b56 --- /dev/null +++ b/src/components/common/MainSpaceList/FloatingBtnController.tsx @@ -0,0 +1,9 @@ +'use client' + +import FloatingButton from '@/components/FloatingButton/FloatingButton' + +const FloatingBtnController = () => { + return +} + +export default FloatingBtnController diff --git a/src/components/common/MainSpaceList/HydrateMainSpaceList.tsx b/src/components/common/MainSpaceList/HydrateMainSpaceList.tsx new file mode 100644 index 00000000..38b421a3 --- /dev/null +++ b/src/components/common/MainSpaceList/HydrateMainSpaceList.tsx @@ -0,0 +1,32 @@ +import { PAGE_SIZE } from '@/constants' +import { getQueryClient } from '@/lib/queryClient' +import { fetchGetSpaces } from '@/services/space/spaces' +import { HydrationBoundary, dehydrate } from '@tanstack/react-query' +import MainSpaceList from './MainSpaceList' + +const HydrateMainSpaceList = () => { + const queryClient = getQueryClient() + void queryClient.prefetchInfiniteQuery({ + queryKey: ['spaces', 'main', { category: '', sort: 'created_at' }], + queryFn: () => + fetchGetSpaces({ + lastSpaceId: undefined, + lastFavoriteCount: undefined, + pageSize: PAGE_SIZE, + sort: 'created_at', + filter: 'all', + }), + initialPageParam: 0, + }) + + const dehydreatedState = dehydrate(queryClient) + console.log(dehydreatedState) + + return ( + + + + ) +} + +export default HydrateMainSpaceList diff --git a/src/components/common/MainSpaceList/MainSpaceHeader.tsx b/src/components/common/MainSpaceList/MainSpaceHeader.tsx new file mode 100644 index 00000000..471a7edd --- /dev/null +++ b/src/components/common/MainSpaceList/MainSpaceHeader.tsx @@ -0,0 +1,23 @@ +'use client' + +import { Suspense } from 'react' +import CategoryListController from '../CategoryList/CategoryListController' +import DropdownController from '../Dropdown/DropdownController' + +const MainSpaceHeader = () => { + return ( +
+
+

스페이스 모음

+ + + +
+ + + +
+ ) +} + +export default MainSpaceHeader diff --git a/src/components/common/MainSpaceList/MainSpaceList.tsx b/src/components/common/MainSpaceList/MainSpaceList.tsx index f67f3201..722becd7 100644 --- a/src/components/common/MainSpaceList/MainSpaceList.tsx +++ b/src/components/common/MainSpaceList/MainSpaceList.tsx @@ -4,57 +4,38 @@ import { Fragment } from 'react' import { NONE_SEARCH_RESULT } from '@/components/SpaceList/constants' import useMainSpacesQuery from '@/components/SpaceList/hooks/useMainSpacesQuery' import { CATEGORIES_RENDER } from '@/constants' +import { useCategoryParam, useSortParam } from '@/hooks' import useInfiniteScroll from '@/hooks/useInfiniteScroll' -import { SearchSpaceReqBody, SpaceResBody } from '@/types' -import Button from '../Button/Button' -import DeferredComponent from '../DeferedComponent/DeferedComponent' +import { fetchGetSpaces } from '@/services/space/spaces' +import { SpaceResBody } from '@/types' +import { useQueryClient } from '@tanstack/react-query' import { MORE_TEXT } from '../LinkList/constants' import Space from '../Space/Space' -import Spinner from '../Spinner/Spinner' export interface SpaceListProps { memberId?: number - queryKey: string - sort?: string - category: string keyword?: string - fetchFn: ({ - memberId, - pageNumber, - lastFavoriteCount, - lastSpaceId, - pageSize, - sort, - filter, - keyWord, - }: SearchSpaceReqBody) => Promise } -const MainSpaceList = ({ - queryKey, - memberId, - sort, - category, - keyword, - fetchFn, -}: SpaceListProps) => { +const MainSpaceList = ({ memberId, keyword }: SpaceListProps) => { + const { sort } = useSortParam('space') + const { category } = useCategoryParam('all') const { spaces, fetchNextPage, hasNextPage, isSpacesLoading } = useMainSpacesQuery({ - queryKey, + queryKey: 'main', memberId, sort, category, keyword, - fetchFn, + fetchFn: fetchGetSpaces, }) + const queryClient = useQueryClient() + console.log(queryClient.getQueryCache()) + const { target } = useInfiniteScroll({ hasNextPage, fetchNextPage }) - return isSpacesLoading ? ( - - - - ) : ( + return ( <>
    { + return ( +
  • + +
  • + ) +} + +const MainSpaceSkeleton = () => { + return ( +
      + + + + + + + + + + +
    + ) +} + +export default MainSpaceSkeleton diff --git a/src/components/common/MainSpaceList/SpaceListController.tsx b/src/components/common/MainSpaceList/SpaceListController.tsx new file mode 100644 index 00000000..9a4f920a --- /dev/null +++ b/src/components/common/MainSpaceList/SpaceListController.tsx @@ -0,0 +1,19 @@ +// 'use client' +import { useCategoryParam, useSortParam } from '@/hooks' +import dynamic from 'next/dynamic' +import DeferredComponent from '../DeferedComponent/DeferedComponent' +import MainSpaceSkeleton from './MainSpaceSkeleton' + +const DynamicMainSpaceList = dynamic(() => import('./MainSpaceList'), { + loading: () => ( + + + + ), +}) + +const SpaceListController = () => { + return +} + +export default SpaceListController diff --git a/src/components/common/SearchModal/SearchModal.tsx b/src/components/common/SearchModal/SearchModal.tsx index a1df3070..49efd6e3 100644 --- a/src/components/common/SearchModal/SearchModal.tsx +++ b/src/components/common/SearchModal/SearchModal.tsx @@ -27,7 +27,6 @@ const SearchModal = ({ onClose }: SearchModalProps) => { }) const searchModalRef = useRef(null) const { - trends, handleOverlayClick, handleTargetChange, handleKeywordClick, diff --git a/src/components/common/SearchModal/hooks/useSearchModal.ts b/src/components/common/SearchModal/hooks/useSearchModal.ts index 0d29acfe..6bf207b8 100644 --- a/src/components/common/SearchModal/hooks/useSearchModal.ts +++ b/src/components/common/SearchModal/hooks/useSearchModal.ts @@ -4,7 +4,6 @@ import { UseFormSetFocus, UseFormSetValue, } from 'react-hook-form' -import { mock_trendData } from '@/data' import { useRouter } from 'next/navigation' import { notify } from '../../Toast/Toast' import { SearchFormValues } from '../SearchModal' @@ -23,7 +22,6 @@ const useSearchModal = ({ onClose, }: useSearchModalProps) => { const router = useRouter() - const trends = mock_trendData useEffect(() => { setFocus('search') @@ -53,7 +51,6 @@ const useSearchModal = ({ } return { - trends, handleTargetChange, handleKeywordClick, handleOverlayClick, diff --git a/src/components/common/Sidebar/Sidebar.tsx b/src/components/common/Sidebar/Sidebar.tsx index 5a0d382d..891dcf76 100644 --- a/src/components/common/Sidebar/Sidebar.tsx +++ b/src/components/common/Sidebar/Sidebar.tsx @@ -89,6 +89,7 @@ const Sidebar = ({ isSidebarOpen, onClose }: SidebarProps) => {
@@ -125,6 +126,7 @@ const Sidebar = ({ isSidebarOpen, onClose }: SidebarProps) => { Object.values(spaces).map(({ spaceId, spaceName }) => (
  • @@ -134,6 +136,7 @@ const Sidebar = ({ isSidebarOpen, onClose }: SidebarProps) => { ))} @@ -141,6 +144,7 @@ const Sidebar = ({ isSidebarOpen, onClose }: SidebarProps) => {
  • @@ -155,6 +159,7 @@ const Sidebar = ({ isSidebarOpen, onClose }: SidebarProps) => { diff --git a/src/components/common/Space/Space.tsx b/src/components/common/Space/Space.tsx index 2637f1ff..44ec0f11 100644 --- a/src/components/common/Space/Space.tsx +++ b/src/components/common/Space/Space.tsx @@ -70,14 +70,17 @@ const Space = ({ <> {type === 'Card' ? ( {spaceImage && ( space-image )}
    @@ -110,6 +113,7 @@ const Space = ({ ) : (
    space-image { return (
    {nickname} diff --git a/src/data/index.ts b/src/data/index.ts deleted file mode 100644 index 8ba6007c..00000000 --- a/src/data/index.ts +++ /dev/null @@ -1,631 +0,0 @@ -import { User } from '@/types' - -export const mock_LinkData = [ - { - id: 1, - title: '자바스크립트 클로저를 활용하는 방법 말줄임표 확인하는 제목입니다', - url: 'https://naver.com', - tagName: '개발', - tagColor: 'pink', - readUsers: [ - { id: 'user1', profile: '/duck.jpg' }, - { id: 'user2', profile: '/duck.jpg' }, - { id: 'user3', profile: '/duck.jpg' }, - { id: 'user4', profile: '/duck.jpg' }, - { id: 'user5', profile: '/duck.jpg' }, - ], - isLiked: false, - likeCount: 7, - }, - { - id: 2, - title: '링크 제목', - url: 'https://github.com', - tagName: '오둥이', - tagColor: 'pink', - readUsers: [ - { id: 'user6', profile: '/duck.jpg' }, - { id: 'user7', profile: '/duck.jpg' }, - { id: 'user8', profile: '/duck.jpg' }, - { id: 'user9', profile: '/duck.jpg' }, - { id: 'user10', profile: '/duck.jpg' }, - ], - isLiked: true, - likeCount: 5, - }, - { - id: 3, - title: '링크 제목', - url: 'https://programmers.co.kr', - tagName: '데브코스', - tagColor: 'pink', - readUsers: [ - { id: 'user11', profile: '/duck.jpg' }, - { id: 'user12', profile: '/duck.jpg' }, - { id: 'user13', profile: '/duck.jpg' }, - { id: 'user14', profile: '/duck.jpg' }, - { id: 'user15', profile: '/duck.jpg' }, - ], - isLiked: false, - likeCount: 1, - }, - { - id: 4, - title: '링크 제목', - url: 'https://nextjs.org/docs/app/api-reference/components/link', - tagName: '개발', - tagColor: 'pink', - readUsers: [ - { id: 'user16', profile: '/duck.jpg' }, - { id: 'user17', profile: '/duck.jpg' }, - { id: 'user18', profile: '/duck.jpg' }, - { id: 'user19', profile: '/duck.jpg' }, - { id: 'user20', profile: '/duck.jpg' }, - ], - isLiked: true, - likeCount: 2, - }, - { - id: 5, - title: '링크 제목', - url: 'https://tailwindcss.com/docs/installation', - tagName: '개발', - tagColor: 'pink', - readUsers: [ - { id: 'user21', profile: '/duck.jpg' }, - { id: 'user22', profile: '/duck.jpg' }, - { id: 'user23', profile: '/duck.jpg' }, - { id: 'user24', profile: '/duck.jpg' }, - { id: 'user25', profile: '/duck.jpg' }, - ], - isLiked: false, - likeCount: 5, - }, - { - id: 6, - title: - '자바스크립트 클로저를 활용하는 방법 말줄임표 확인하는 제목입니다 자바스크립트 클로저를 활용하는 방법 말줄임표 확인하는 제목입니다', - url: 'https://velog.io/', - tagName: '개발', - tagColor: 'pink', - readUsers: [], - isLiked: true, - likeCount: 3, - }, -] - -export const mock_memberData = [ - { - memberId: 1, - nickname: '오둥이', - aboutMe: '안녕하세요', - profilePath: '/duck.jpg', - SpaceMemberRole: 'OWNER', - }, - { - memberId: 2, - nickname: '백둥이', - aboutMe: '안녕하세요', - profilePath: '/duck.jpg', - SpaceMemberRole: 'CANVIEW', - }, - { - memberId: 3, - nickname: '풀택이', - aboutMe: '안녕하세요', - profilePath: '/duck.jpg', - SpaceMemberRole: 'CANVIEW', - }, -] - -export const mock_userData: User = { - id: '3', - name: '프롱이', - profile: '/duck.jpg', - category: 'ENTER_ART', - newsLetter: false, - email: 'abc@gmail.com', - description: '쇼핑 정보를 모으고 있어요!', - follower: 138, - following: 182, - mySpaces: [ - { - name: 'My Space 1', - id: 'my-space-1', - }, - { - name: 'My Space 2', - id: 'my-space-2', - }, - { - name: 'My Space 3', - id: 'my-space-3', - }, - { - name: 'My Space 4', - id: 'my-space-4', - }, - { - name: 'My Space 5', - id: 'my-space-5', - }, - ], - favoriteSpaces: [ - { - name: 'Favorite Space 1', - id: 'favorite-space-1', - }, - { - name: 'Favorite Space 2', - id: 'favorite-space-2', - }, - { - name: 'Favorite Space 3', - id: 'favorite-space-3', - }, - { - name: 'Favorite Space 4', - id: 'favorite-space-4', - }, - { - name: 'Favorite Space 5', - id: 'favorite-space-5', - }, - ], -} - -export const mock_userData2 = { - id: 6, - name: '프롱이', - profile: '/duck.jpg', - email: 'abc@gmail.com', - category: '생활•노하우•쇼핑', - description: '쇼핑 정보를 모으고 있어요!', - follower: [ - { - userId: 1, - userName: '백둥이', - profile: '/duck.jpg', - description: '안녕 난 백둥이', - isFollow: false, - }, - { - userId: 2, - userName: '풀택이', - profile: '/duck.jpg', - description: '안녕 난 풀택이', - isFollow: true, - }, - { - userId: 3, - userName: '오둥이', - profile: '/duck.jpg', - description: '안녕 난 오둥이', - isFollow: false, - }, - { - userId: 123, - userName: '프롱이', - profile: '/duck.jpg', - description: '안녕 난 프롱이', - isFollow: false, - }, - ], - - following: [ - { - userId: 1, - userName: '육둥이', - profile: '/duck.jpg', - description: '안녕 난 육둥이', - isFollow: false, - }, - { - userId: 2, - userName: '칠둥이', - profile: '/duck.jpg', - description: '안녕 난 칠둥이', - isFollow: true, - }, - { - userId: 3, - userName: '팔둥이', - profile: '/duck.jpg', - description: '안녕 난 팔둥이', - isFollow: false, - }, - { - userId: 123, - userName: '프롱이', - profile: '/duck.jpg', - description: '안녕 난 프롱이', - isFollow: false, - }, - ], - mySpaces: [ - { - userName: '프롱이', - spaceId: 1, - spaceImage: '/TestImage.svg', - spaceName: '강남역 맛집 리스트 모음 스페이스', - description: '내 기준 강남역에서 맛있는 맛집 링크 모음집', - category: '생활•노하우•쇼핑', - favorite: 60, - scrap: 40, - comment: true, - }, - { - userName: '프롱이', - spaceId: 2, - spaceImage: '/TestImage.svg', - spaceName: '홍대역 맛집 리스트 모음 스페이스', - description: '내 기준 홍대역에서 맛있는 맛집 링크 모음집', - category: '생활•노하우•쇼핑', - favorite: 50, - scrap: 40, - comment: true, - }, - { - userName: '프롱이', - spaceId: 3, - spaceImage: '/TestImage.svg', - spaceName: '구리역 맛집 리스트 모음 스페이스', - description: '내 기준 구리역에서 맛있는 맛집 링크 모음집', - category: '생활•노하우•쇼핑', - favorite: 40, - scrap: 40, - comment: true, - }, - { - userName: '프롱이', - spaceId: 4, - spaceImage: '/TestImage.svg', - spaceName: '신촌역 맛집 리스트 모음 스페이스', - description: '내 기준 신촌역에서 맛있는 맛집 링크 모음집', - category: '생활•노하우•쇼핑', - favorite: 30, - scrap: 40, - comment: true, - }, - { - userName: '프롱이', - spaceId: 5, - spaceImage: '/TestImage.svg', - spaceName: '수원역 맛집 리스트 모음 스페이스', - description: '내 기준 수원역에서 맛있는 맛집 링크 모음집', - category: '생활•노하우•쇼핑', - favorite: 20, - scrap: 40, - comment: true, - }, - ], - favoriteSpaces: [ - { - userName: '백둥이', - spaceId: 1, - spaceImage: '/TestImage.svg', - spaceName: '스프링 지식 모음 스페이스', - description: '스프링 관련 링크 모음집', - category: '생활•노하우•쇼핑', - favorite: 60, - scrap: 40, - comment: true, - }, - { - userName: '백둥이', - spaceId: 2, - spaceImage: '/TestImage.svg', - spaceName: '코틀린 지식 모음 스페이스', - description: '코틀린 관련 링크 모음집', - category: '생활•노하우•쇼핑', - favorite: 50, - scrap: 40, - comment: true, - }, - { - userName: '풀택이', - spaceId: 3, - spaceImage: '/TestImage.svg', - spaceName: '클린 코드 모음 스페이스', - description: '클린 코드 관련 링크 모음집', - category: '생활•노하우•쇼핑', - favorite: 40, - scrap: 40, - comment: true, - }, - { - userName: '백둥이', - spaceId: 4, - spaceImage: '/TestImage.svg', - spaceName: '객체지향 모음 스페이스', - description: '객체지향 관련 링크 모음집', - category: '생활•노하우•쇼핑', - favorite: 30, - scrap: 40, - comment: true, - }, - { - userName: '풀택이', - spaceId: 5, - spaceImage: '/TestImage.svg', - spaceName: '알고리즘 모음 스페이스', - description: '알고리즘 관련 링크 모음집', - category: '생활•노하우•쇼핑', - favorite: 20, - scrap: 40, - comment: true, - }, - ], -} - -export const mock_spaceData = { - spaceId: 108, - spaceName: '강남역 맛집 리스트 모음 스페이스', - description: '내 기준 강남역에서 맛있는 맛집 링크 모음집', - category: 'ENTER_ART', - isVisible: true, - isComment: true, - isLinkSummarizable: true, - isReadMarkEnabled: true, - viewCount: 0, - scrapCount: 0, - favoriteCount: 0, - spaceImagePath: '/TestImage.svg', - isOwner: true, - isCanEdit: true, - hasFavorite: true, - memberDetailInfos: [ - { - memberId: 0, - nickname: 'string', - aboutMe: 'string', - profilePath: 'string', - SpaceMemberRole: 'OWNER', - }, - ], -} - -export const mock_notificationData: { - id: number - notificationType: 'COMMENT' | 'FOLLOW' | 'INVITATION' - userId: number - userName: string - spaceId?: number - spaceName?: string - isRead: boolean - isAccept?: boolean -}[] = [ - { - id: 1, - notificationType: 'FOLLOW', - userId: 1, - userName: '프롱이', - isRead: false, - }, - { - id: 2, - notificationType: 'COMMENT', - userId: 1, - userName: '프롱이', - spaceId: 123, - spaceName: '리액트 모음집', - isRead: false, - isAccept: false, - }, - { - id: 3, - notificationType: 'COMMENT', - userId: 1, - userName: '백둥이', - spaceId: 456, - spaceName: '스프링', - isRead: false, - isAccept: false, - }, - { - id: 4, - notificationType: 'FOLLOW', - userId: 1, - userName: '백둥이', - isRead: true, - }, - { - id: 5, - notificationType: 'COMMENT', - userId: 1, - userName: '풀택이', - spaceId: 789, - spaceName: '풀스택 지식 모음집', - isRead: true, - isAccept: false, - }, -] - -export const mock_notificationInviteData: { - id: number - type: 'comment' | 'follow' | 'space' - userId: number - userName: string - spaceId?: number - spaceName?: string - isRead: boolean - isAccept?: boolean -}[] = [ - { - id: 3, - type: 'space', - userId: 1, - userName: '프롱이', - spaceId: 123, - spaceName: '개발 모음', - isRead: false, - isAccept: false, - }, - { - id: 4, - type: 'space', - userId: 1, - userName: '백둥이', - spaceId: 123, - spaceName: '스프링', - isRead: false, - isAccept: false, - }, - { - id: 5, - type: 'space', - userId: 1, - userName: '풀택이', - spaceId: 123, - spaceName: '풀스택 지식 모음집', - isRead: true, - isAccept: true, - }, -] - -export const mock_commentData = [ - { - commentId: 1, - user: { id: 1, name: '프롱이', profile: '/duck.jpg' }, - comment: '어쩌구', - date: new Date(), - auth: true, - replyCount: 0, - }, - { - commentId: 2, - user: { id: 2, name: '백둥이', profile: '/duck.jpg' }, - comment: '저쩌구', - date: new Date(), - auth: false, - replyCount: 2, - }, -] - -export const mock_replyData = [ - { - commentId: 3, - user: { id: 3, name: '풀택이', profile: '/duck.jpg' }, - comment: '쏼라쏼라', - date: new Date(), - auth: false, - }, - { - commentId: 4, - user: { id: 1, name: '프롱이', profile: '/duck.jpg' }, - comment: '훌라훌라', - date: new Date(), - auth: true, - }, -] - -export const mock_trendData = [ - { - keyword: '어쩌구', - }, - { - keyword: '저쩌구', - }, - { - keyword: '쏼라쏼라', - }, - { - keyword: '훌라훌라', - }, - { - keyword: '나하항', - }, -] - -export const mock_usersData = [ - { - id: 1, - name: 'dudwns', - oneLiner: '안녕하세요', - profile: '/duck.jpg', - isFollow: true, - }, - { - id: 2, - name: 'bomi', - oneLiner: '안녕하세요', - profile: '/duck.jpg', - isFollow: false, - }, - { - id: 3, - name: '프롱프롱프롱프롱프롱프롱프롱프롱프롱프롱프롱프롱프롱프롱프롱프롱프롱프롱이', - oneLiner: - '안녕하세요안녕하세요안녕하세요안녕하세요안녕하세요안녕하세요안녕하세요안녕하세요안녕하세요안녕하세요', - profile: '/duck.jpg', - }, -] - -export const mock_spacesData = [ - { - userName: 'frong', - spaceId: 123, - spaceImage: '/TestImage.svg', - spaceName: '강남역 맛집 리스트 모음 스페이스', - description: '내 기준 강남역에서 맛있는 맛집 링크 모음집', - category: '생활•노하우•쇼핑', - favorite: 60, - scrap: 40, - comment: true, - }, - { - userName: 'backdung', - spaceId: 456, - spaceImage: '/TestImage.svg', - spaceName: '역삼역 맛집 리스트 모음 스페이스', - description: '내 기준 역삼역에서 맛있는 맛집 링크 모음집', - category: '생활•노하우•쇼핑', - favorite: 0, - scrap: 10, - comment: true, - }, - { - userName: '프롱', - spaceId: 0, - spaceImage: '/TestImage.svg', - spaceName: '삼성역 맛집 리스트 모음 스페이스', - description: '내 기준 삼성역에서 맛있는 맛집 링크 모음집', - category: '생활•노하우•쇼핑', - favorite: 99, - scrap: 24, - comment: true, - }, - { - userName: '백둥', - spaceId: 1, - spaceImage: '/TestImage.svg', - spaceName: '삼성역 맛집 리스트 모음 스페이스', - description: '내 기준 삼성역에서 맛있는 맛집 링크 모음집', - category: '생활•노하우•쇼핑', - favorite: 99, - scrap: 24, - comment: true, - }, - { - userName: '머쓱', - spaceId: 2, - spaceImage: '/TestImage.svg', - spaceName: '삼성역 맛집 리스트 모음 스페이스', - description: '내 기준 삼성역에서 맛있는 맛집 링크 모음집', - category: '생활•노하우•쇼핑', - favorite: 99, - scrap: 24, - comment: true, - }, - { - userName: '타드', - spaceId: 3, - spaceImage: '/TestImage.svg', - spaceName: '삼성역 맛집 리스트 모음 스페이스', - description: '내 기준 삼성역에서 맛있는 맛집 링크 모음집', - category: '생활•노하우•쇼핑', - favorite: 99, - scrap: 24, - comment: true, - }, -] diff --git a/src/hooks/useCategoryParam.ts b/src/hooks/useCategoryParam.ts index 56ec074a..9a6704da 100644 --- a/src/hooks/useCategoryParam.ts +++ b/src/hooks/useCategoryParam.ts @@ -6,7 +6,7 @@ const useCategoryParam = (type: keyof typeof CATEGORIES) => { const router = useRouter() const pathname = usePathname() const searchParams = useSearchParams() - const category = searchParams.get('category') + const category = searchParams.get('category') || '' const categoryIndex = category ? Object.values(CATEGORIES[type]).indexOf(category) : 0 diff --git a/src/hooks/useGetPopularLinks.ts b/src/hooks/useGetPopularLinks.ts deleted file mode 100644 index d29bcddd..00000000 --- a/src/hooks/useGetPopularLinks.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { useEffect, useState } from 'react' -import { fetchGetPopularLinks } from '@/services/link/link' -import { PopularLinkResBody } from '@/types' - -const useGetPopularLinks = () => { - const [links, setLinks] = useState() - const [isLoading, setIsLoading] = useState(false) - - useEffect(() => { - const fetchData = async () => { - setIsLoading(true) - const data = await fetchGetPopularLinks() - const linkData = data.responses - setLinks(linkData) - setIsLoading(false) - } - fetchData() - }, [setIsLoading]) - - return { links, isPopularLinksLoading: isLoading } -} - -export default useGetPopularLinks diff --git a/src/hooks/useSortParam.ts b/src/hooks/useSortParam.ts index f0bab206..3d04f760 100644 --- a/src/hooks/useSortParam.ts +++ b/src/hooks/useSortParam.ts @@ -6,7 +6,7 @@ const useSortParam = (type: keyof typeof DROPDOWN_OPTIONS) => { const router = useRouter() const pathname = usePathname() const searchParams = useSearchParams() - const sort = searchParams.get('sort') + const sort = searchParams.get('sort') || undefined const sortIndex = sort ? Object.values(DROPDOWN_OPTIONS[type]).indexOf(sort) : 0 diff --git a/src/lib/contexts/TanstackQueryContext.tsx b/src/lib/contexts/TanstackQueryContext.tsx index 3c43be53..bb93d52f 100644 --- a/src/lib/contexts/TanstackQueryContext.tsx +++ b/src/lib/contexts/TanstackQueryContext.tsx @@ -1,7 +1,8 @@ 'use client' import { useState } from 'react' -import { QueryClient, QueryClientProvider } from '@tanstack/react-query' +import { QueryClientProvider } from '@tanstack/react-query' +import { getQueryClient } from '../queryClient' interface TanstackQueryContextProps { children: React.ReactNode @@ -10,7 +11,7 @@ interface TanstackQueryContextProps { export default function TanstackQueryContext({ children, }: TanstackQueryContextProps) { - const [queryClient] = useState(() => new QueryClient()) + const [queryClient] = useState(getQueryClient()) return ( {children} diff --git a/src/lib/queryClient.ts b/src/lib/queryClient.ts new file mode 100644 index 00000000..57eb4f7e --- /dev/null +++ b/src/lib/queryClient.ts @@ -0,0 +1,31 @@ +import { + QueryClient, + defaultShouldDehydrateQuery, + isServer, +} from '@tanstack/react-query' + +function makeQueryClient() { + return new QueryClient({ + defaultOptions: { + queries: { + staleTime: 60 * 1000, + }, + dehydrate: { + shouldDehydrateQuery: (query) => + defaultShouldDehydrateQuery(query) || + query.state.status === 'pending', + }, + }, + }) +} + +let browserQueryClient: QueryClient | undefined = undefined + +export function getQueryClient() { + if (isServer) { + return makeQueryClient() + } else { + if (!browserQueryClient) browserQueryClient = makeQueryClient() + return browserQueryClient + } +} diff --git a/src/services/space/spaces.ts b/src/services/space/spaces.ts index 32b4ed16..4d89cc6e 100644 --- a/src/services/space/spaces.ts +++ b/src/services/space/spaces.ts @@ -1,12 +1,13 @@ +import { PAGE_SIZE } from '@/constants' import { SearchSpaceReqBody, SpaceInviteResBody } from '@/types' import { apiClient } from '../apiServices' const fetchGetSpaces = async ({ - lastSpaceId, - lastFavoriteCount, - pageSize, - sort, - filter, + lastSpaceId = undefined, + lastFavoriteCount = undefined, + pageSize = PAGE_SIZE, + sort = 'created_at', + filter = 'all', }: SearchSpaceReqBody) => { const path = '/api/spaces' const params = {