diff --git a/Mine/src/api/magazine.ts b/Mine/src/api/magazine.ts index b13d6e6..8af9054 100644 --- a/Mine/src/api/magazine.ts +++ b/Mine/src/api/magazine.ts @@ -12,6 +12,8 @@ import type { ResponseAddSectionInSectionPage, ResponseFeed, ResponseRecentSection, + SearchLikedMagazinePageParams, + SearchMagazineFeed, SectionDetailDto, } from '../types/magazine' import type { MyMagazinesDto, ResponseMyMagazine } from '../types/magazine' @@ -122,3 +124,17 @@ export const patchSection = async ({ magazineId, sectionId, heading, paragraphs }) return res.data } + +export const getSearchLikedMagazine = async ({ keyword, page = 0, size = 10 }: SearchLikedMagazinePageParams) => { + const res = await axiosInstance.get(`api/magazines/liked/search`, { + params: { keyword, page, size }, + }) + return res.data +} + +export const getSearchMagazineFeed = async ({ keyword, page = 0, size = 10 }: SearchMagazineFeed) => { + const res = await axiosInstance.get(`api/magazines/liked/search`, { + params: { keyword, page, size }, + }) + return res.data +} diff --git a/Mine/src/components/skeleton/ExploreSkeleton.tsx b/Mine/src/components/skeleton/ExploreSkeleton.tsx index 51620bd..6028001 100644 --- a/Mine/src/components/skeleton/ExploreSkeleton.tsx +++ b/Mine/src/components/skeleton/ExploreSkeleton.tsx @@ -1,24 +1,21 @@ import { SkeletonBox } from '../../components/skeleton/SkeletonBase' -import useSidebarStore from '../../stores/sidebar' export default function ExploreSkeleton() { - const { isOpen } = useSidebarStore() return ( -
-
+
- - - - - - - - - + + + + + + + + +
-
+
{' '}
) } diff --git a/Mine/src/hooks/useGetSearchLikedMagazine.ts b/Mine/src/hooks/useGetSearchLikedMagazine.ts new file mode 100644 index 0000000..f0a6d3a --- /dev/null +++ b/Mine/src/hooks/useGetSearchLikedMagazine.ts @@ -0,0 +1,25 @@ +import { useInfiniteQuery, type InfiniteData } from '@tanstack/react-query' +import { getSearchLikedMagazine } from '../api/magazine' +import type { SearchLikedMagazineResponse } from '../types/magazine' +import type { AxiosError } from 'axios' + +export const useGetSearchLikedMagazines = (keyword: string) => { + return useInfiniteQuery< + SearchLikedMagazineResponse, + AxiosError, + InfiniteData, + [string, string], + number + >({ + queryKey: ['searchLikedMagazines', keyword], + + queryFn: ({ pageParam }) => getSearchLikedMagazine({ keyword, page: pageParam }), + initialPageParam: 0, + getNextPageParam: (lastPage) => { + return lastPage.last ? undefined : lastPage.number + 1 + }, + + // 키워드가 빈 문자열이 아닐 때만 API를 호출하도록 설정 (선택 사항) + enabled: !!keyword, + }) +} diff --git a/Mine/src/hooks/useGetSearchMagazineFeed.ts b/Mine/src/hooks/useGetSearchMagazineFeed.ts new file mode 100644 index 0000000..88737f9 --- /dev/null +++ b/Mine/src/hooks/useGetSearchMagazineFeed.ts @@ -0,0 +1,25 @@ +import { useInfiniteQuery, type InfiniteData } from '@tanstack/react-query' +import { getSearchMagazineFeed } from '../api/magazine' +import type { SearchMagazineFeedResponse } from '../types/magazine' +import type { AxiosError } from 'axios' + +export const useGetSearchMagazineFeed = (keyword: string) => { + return useInfiniteQuery< + SearchMagazineFeedResponse, + AxiosError, + InfiniteData, + [string, string], + number + >({ + queryKey: ['searchLikedMagazines', keyword], + + queryFn: ({ pageParam }) => getSearchMagazineFeed({ keyword, page: pageParam }), + initialPageParam: 0, + getNextPageParam: (lastPage) => { + return lastPage.last ? undefined : lastPage.number + 1 + }, + + // 키워드가 빈 문자열이 아닐 때만 API를 호출하도록 설정 (선택 사항) + enabled: !!keyword, + }) +} diff --git a/Mine/src/pages/magazine/ExplorePage.tsx b/Mine/src/pages/magazine/ExplorePage.tsx index 681a252..8b04022 100644 --- a/Mine/src/pages/magazine/ExplorePage.tsx +++ b/Mine/src/pages/magazine/ExplorePage.tsx @@ -5,21 +5,46 @@ import ExploreSkeleton from '../../components/skeleton/ExploreSkeleton' import explorebg from '../../assets/explorebg.jpg' import useSidebarStore from '../../stores/sidebar' import SearchInput from '../../components/common/SearchInput' +import { useGetSearchMagazineFeed } from '../../hooks/useGetSearchMagazineFeed' export default function ExplorePage() { const { isOpen } = useSidebarStore() - const { data, fetchNextPage, hasNextPage, isFetchingNextPage, isLoading } = useGetMagazineFeed() const [searchValue, setSearchValue] = useState('') + const { + data: magazinedata, + fetchNextPage: magazineFetchNextPage, + hasNextPage: magazineHasNextPage, + isFetchingNextPage: magazineIsFetchingNextPage, + isLoading: magazineIsLoading, + } = useGetMagazineFeed() + const { + data: searchData, + fetchNextPage: searchFetchNextPage, + hasNextPage: searchHasNextPage, + isFetchingNextPage: searchIsFetchingNextPage, + isLoading: searchIsLoading, + } = useGetSearchMagazineFeed(searchValue) + const observerRef = useRef(null) - const magazines = data?.pages.flatMap((page) => page.content) ?? [] + const isSearching = searchValue.trim() !== '' + + const defaultMagazines = magazinedata?.pages.flatMap((page) => page.content) ?? [] + const searchMagazines = searchData?.pages.flatMap((page) => page.content) ?? [] + + const activeMagazines = isSearching ? searchMagazines : defaultMagazines + + const activeHasNextPage = isSearching ? searchHasNextPage : magazineHasNextPage + const activeIsFetchingNextPage = isSearching ? searchIsFetchingNextPage : magazineIsFetchingNextPage + const activeFetchNextPage = isSearching ? searchFetchNextPage : magazineFetchNextPage + const activeIsLoading = isSearching ? searchIsLoading : magazineIsLoading useEffect(() => { const observer = new IntersectionObserver( (entries) => { - if (entries[0].isIntersecting && hasNextPage && !isFetchingNextPage) { - fetchNextPage() + if (entries[0].isIntersecting && activeHasNextPage && !activeIsFetchingNextPage) { + activeFetchNextPage() } }, { threshold: 0.1 } @@ -27,9 +52,9 @@ export default function ExplorePage() { if (observerRef.current) observer.observe(observerRef.current) return () => observer.disconnect() - }, [hasNextPage, isFetchingNextPage, fetchNextPage]) + }, [activeHasNextPage, activeIsFetchingNextPage, activeFetchNextPage]) - if (isLoading) return + // if (activeIsLoading) return return (
-
+
- -
- {isFetchingNextPage &&
로딩중...
} + {!activeIsLoading && ( + <> + +
+ {magazineIsFetchingNextPage &&
로딩중...
} + + )} + {activeIsLoading && }
diff --git a/Mine/src/pages/magazine/SavedMagazinePage.tsx b/Mine/src/pages/magazine/SavedMagazinePage.tsx index 3e461db..6984523 100644 --- a/Mine/src/pages/magazine/SavedMagazinePage.tsx +++ b/Mine/src/pages/magazine/SavedMagazinePage.tsx @@ -9,6 +9,7 @@ import useSidebarStore from '../../stores/sidebar' import SearchInput from '../../components/common/SearchInput' import MineLogo from '../../icon/logo_with_title.svg?react' import type { Magazine } from '../../types/magazine' +import { useGetSearchLikedMagazines } from '../../hooks/useGetSearchLikedMagazine' const CARD_WIDTH = 476 const GAP = 16 @@ -38,18 +39,30 @@ export default function SavedMagazinePage() { const [columnIndex, setColumnIndex] = useState(0) const { isOpen } = useSidebarStore() const [searchValue, setSearchValue] = useState('') + const [isAnimating, setIsAnimating] = useState(true) const navigate = useNavigate() - const { data, isLoading, isError } = useGetLikedMagazineList({ + const { + data: baseData, + isLoading: isBaseLoading, + isError, + } = useGetLikedMagazineList({ page: 0, size: 20, sort: ['createdAt,desc'], }) - if (isLoading) return + const { data: searchData, fetchNextPage, hasNextPage } = useGetSearchLikedMagazines(searchValue) + + const baseMagazines = baseData?.content ?? [] + const searchMagazines = searchData?.pages.flatMap((page) => page.content) ?? [] + + const isSearching = searchValue.trim() != '' + const magazines = isSearching ? searchMagazines : baseMagazines + + if (isBaseLoading) return if (isError) return
불러오기 실패
- const magazines = data?.content ?? [] const isEmpty = magazines.length === 0 const totalSlots = Math.max(magazines.length, MIN_CARDS) @@ -62,6 +75,27 @@ export default function SavedMagazinePage() { const TOTAL_PAGES = Math.ceil(TOTAL_COLUMNS / COLUMNS_PER_VIEW) const firstEmptyIndex = allItems.findIndex((item) => item === null) + const handleSearchChange = (value: string) => { + setIsAnimating(false) + setSearchValue(value) + setColumnIndex(0) + } + const handleNextClick = () => { + setIsAnimating(true) + const nextIndex = Math.min(columnIndex + 1, TOTAL_PAGES - 1) + setColumnIndex(nextIndex) + + if (isSearching && hasNextPage) { + if (nextIndex >= TOTAL_PAGES - 2) { + fetchNextPage() + } + } + } + const handlePrevClick = () => { + setIsAnimating(true) + setColumnIndex((prev) => Math.max(prev - 1, 0)) + } + if (isEmpty) { return (
- +
@@ -122,15 +156,15 @@ export default function SavedMagazinePage() { >
- +
{columns.map((col, colIdx) => (
@@ -155,8 +189,8 @@ export default function SavedMagazinePage() { setColumnIndex((prev) => Math.min(prev + 1, TOTAL_PAGES - 1))} - onPrev={() => setColumnIndex((prev) => Math.max(prev - 1, 0))} + onNext={handleNextClick} + onPrev={handlePrevClick} />
diff --git a/Mine/src/types/magazine.ts b/Mine/src/types/magazine.ts index 17ed1f9..d6c19e4 100644 --- a/Mine/src/types/magazine.ts +++ b/Mine/src/types/magazine.ts @@ -169,3 +169,40 @@ export type PatchSectionDto = { imageUrl: string }[] } + +type SearchBase = { + keyword: string + page?: number + size?: number +} + +export type SearchLikedMagazinePageParams = SearchBase + +export type SearchMagazineFeed = SearchBase + +type Content = { + title: string + coverImageUrl: string + username: string + likeCount: number + commentCount: number + createdAt: string + moodboard: Moodboard + magazineId: number +} + +type SearchResponse = { + content: Content[] + totalPages: number + totalElements: number + last: boolean + size: number + number: number + numberOfElements: number + first: boolean + empty: boolean +} + +export type SearchLikedMagazineResponse = SearchResponse + +export type SearchMagazineFeedResponse = SearchResponse