From c9e4b01d3f18785889bf7cfff912fcca4914b672 Mon Sep 17 00:00:00 2001 From: yoonvinjeong Date: Tue, 12 May 2026 14:11:48 +0900 Subject: [PATCH 1/3] =?UTF-8?q?feat:=20=EC=A0=80=EC=9E=A5=ED=95=9C=20?= =?UTF-8?q?=EB=A7=A4=EA=B1=B0=EC=A7=84=20=EA=B2=80=EC=83=89=20API=20?= =?UTF-8?q?=EC=97=B0=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Mine/src/api/magazine.ts | 8 +++ Mine/src/hooks/useGetSearchLikedMagazine.ts | 25 +++++++++ Mine/src/pages/magazine/SavedMagazinePage.tsx | 52 +++++++++++++++---- Mine/src/types/magazine.ts | 29 +++++++++++ 4 files changed, 105 insertions(+), 9 deletions(-) create mode 100644 Mine/src/hooks/useGetSearchLikedMagazine.ts diff --git a/Mine/src/api/magazine.ts b/Mine/src/api/magazine.ts index b13d6e6..35c1751 100644 --- a/Mine/src/api/magazine.ts +++ b/Mine/src/api/magazine.ts @@ -12,6 +12,7 @@ import type { ResponseAddSectionInSectionPage, ResponseFeed, ResponseRecentSection, + SearchLikedMagazinePageParams, SectionDetailDto, } from '../types/magazine' import type { MyMagazinesDto, ResponseMyMagazine } from '../types/magazine' @@ -122,3 +123,10 @@ 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 +} 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/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..0bd05f7 100644 --- a/Mine/src/types/magazine.ts +++ b/Mine/src/types/magazine.ts @@ -169,3 +169,32 @@ export type PatchSectionDto = { imageUrl: string }[] } + +export type SearchLikedMagazinePageParams = { + keyword: string + page?: number + size?: number +} + +type Content = { + title: string + coverImageUrl: string + username: string + likeCount: number + commentCount: number + createdAt: string + moodboard: Moodboard + magazineId: number +} + +export type SearchLikedMagazineResponse = { + content: Content[] + totalPages: number + totalElements: number + last: boolean + size: number + number: number + numberOfElements: number + first: boolean + empty: boolean +} From 5ed59b51ffc41015e78140b948ee8787a73dba60 Mon Sep 17 00:00:00 2001 From: yoonvinjeong Date: Tue, 12 May 2026 14:36:10 +0900 Subject: [PATCH 2/3] =?UTF-8?q?feat:=20=ED=94=BC=EB=93=9C=20=EA=B2=80?= =?UTF-8?q?=EC=83=89=20API=20=EC=97=B0=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Mine/src/api/magazine.ts | 8 +++++ Mine/src/hooks/useGetSearchMagazineFeed.ts | 25 +++++++++++++ Mine/src/pages/magazine/ExplorePage.tsx | 41 +++++++++++++++++----- Mine/src/types/magazine.ts | 12 +++++-- 4 files changed, 76 insertions(+), 10 deletions(-) create mode 100644 Mine/src/hooks/useGetSearchMagazineFeed.ts diff --git a/Mine/src/api/magazine.ts b/Mine/src/api/magazine.ts index 35c1751..8af9054 100644 --- a/Mine/src/api/magazine.ts +++ b/Mine/src/api/magazine.ts @@ -13,6 +13,7 @@ import type { ResponseFeed, ResponseRecentSection, SearchLikedMagazinePageParams, + SearchMagazineFeed, SectionDetailDto, } from '../types/magazine' import type { MyMagazinesDto, ResponseMyMagazine } from '../types/magazine' @@ -130,3 +131,10 @@ export const getSearchLikedMagazine = async ({ keyword, page = 0, size = 10 }: S }) 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/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..aed585d 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 &&
로딩중...
} + {magazineIsFetchingNextPage &&
로딩중...
}
diff --git a/Mine/src/types/magazine.ts b/Mine/src/types/magazine.ts index 0bd05f7..d6c19e4 100644 --- a/Mine/src/types/magazine.ts +++ b/Mine/src/types/magazine.ts @@ -170,12 +170,16 @@ export type PatchSectionDto = { }[] } -export type SearchLikedMagazinePageParams = { +type SearchBase = { keyword: string page?: number size?: number } +export type SearchLikedMagazinePageParams = SearchBase + +export type SearchMagazineFeed = SearchBase + type Content = { title: string coverImageUrl: string @@ -187,7 +191,7 @@ type Content = { magazineId: number } -export type SearchLikedMagazineResponse = { +type SearchResponse = { content: Content[] totalPages: number totalElements: number @@ -198,3 +202,7 @@ export type SearchLikedMagazineResponse = { first: boolean empty: boolean } + +export type SearchLikedMagazineResponse = SearchResponse + +export type SearchMagazineFeedResponse = SearchResponse From 5e1146d9f443b1d19bbed66f1b9323795e760c2e Mon Sep 17 00:00:00 2001 From: yoonvinjeong Date: Tue, 12 May 2026 15:08:21 +0900 Subject: [PATCH 3/3] =?UTF-8?q?style:=20=EB=91=98=EB=9F=AC=EB=B3=B4?= =?UTF-8?q?=EA=B8=B0=20=ED=8E=98=EC=9D=B4=EC=A7=80=20=EC=8A=A4=EC=BC=88?= =?UTF-8?q?=EB=A0=88=ED=86=A4,=20=EB=B0=B0=EA=B2=BD=20=EC=84=A4=EC=A0=95?= =?UTF-8?q?=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/skeleton/ExploreSkeleton.tsx | 25 ++++++++----------- Mine/src/pages/magazine/ExplorePage.tsx | 15 +++++++---- 2 files changed, 21 insertions(+), 19 deletions(-) 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/pages/magazine/ExplorePage.tsx b/Mine/src/pages/magazine/ExplorePage.tsx index aed585d..8b04022 100644 --- a/Mine/src/pages/magazine/ExplorePage.tsx +++ b/Mine/src/pages/magazine/ExplorePage.tsx @@ -54,7 +54,7 @@ export default function ExplorePage() { return () => observer.disconnect() }, [activeHasNextPage, activeIsFetchingNextPage, activeFetchNextPage]) - if (activeIsLoading) return + // if (activeIsLoading) return return (
-
+
- -
- {magazineIsFetchingNextPage &&
로딩중...
} + {!activeIsLoading && ( + <> + +
+ {magazineIsFetchingNextPage &&
로딩중...
} + + )} + {activeIsLoading && }