From bc9117c525add136c4525aca5671987cc946f9dd Mon Sep 17 00:00:00 2001 From: shinwookkang Date: Wed, 25 Feb 2026 13:29:25 +0900 Subject: [PATCH 01/10] feat: integrate My Book Stories API with infinite scroll and conditional buttons --- .../base-ui/BookStory/bookstory_card.tsx | 18 +++--- .../base-ui/MyPage/MyBookStoryList.tsx | 59 +++++++++++++++---- src/hooks/queries/useStoryQueries.ts | 13 ++++ src/lib/api/endpoints/bookstory.ts | 1 + src/services/storyService.ts | 9 +++ 5 files changed, 83 insertions(+), 17 deletions(-) diff --git a/src/components/base-ui/BookStory/bookstory_card.tsx b/src/components/base-ui/BookStory/bookstory_card.tsx index 2e8dbb7..496ede4 100644 --- a/src/components/base-ui/BookStory/bookstory_card.tsx +++ b/src/components/base-ui/BookStory/bookstory_card.tsx @@ -14,6 +14,7 @@ type Props = { commentCount?: number; onSubscribeClick?: () => void; subscribeText?: string; + hideSubscribeButton?: boolean; }; import { formatTimeAgo } from "@/utils/time"; @@ -30,6 +31,7 @@ export default function BookStoryCard({ commentCount = 1, onSubscribeClick, subscribeText = "구독", + hideSubscribeButton = false, }: Props) { return (
- + {!hideSubscribeButton && ( + + )} {/* 2. 책 이미지 (모바일: flex-1 / 데스크탑: h-36) */} diff --git a/src/components/base-ui/MyPage/MyBookStoryList.tsx b/src/components/base-ui/MyPage/MyBookStoryList.tsx index 758ef03..43d93d3 100644 --- a/src/components/base-ui/MyPage/MyBookStoryList.tsx +++ b/src/components/base-ui/MyPage/MyBookStoryList.tsx @@ -1,28 +1,67 @@ "use client"; -import React from "react"; +import React, { useEffect } from "react"; import BookStoryCard from "@/components/base-ui/BookStory/bookstory_card"; -import { DUMMY_MY_STORIES } from "@/constants/mocks/mypage"; +import { useMyInfiniteStoriesQuery } from "@/hooks/queries/useStoryQueries"; +import { useInView } from "react-intersection-observer"; const MyBookStoryList = () => { + const { + data, + fetchNextPage, + hasNextPage, + isFetchingNextPage, + isLoading, + isError, + } = useMyInfiniteStoriesQuery(); + + const { ref, inView } = useInView(); + + useEffect(() => { + if (inView && hasNextPage && !isFetchingNextPage) { + fetchNextPage(); + } + }, [inView, hasNextPage, isFetchingNextPage, fetchNextPage]); + + const stories = data?.pages.flatMap((page) => page.basicInfoList) || []; + return (
+ + {isLoading &&

로딩 중...

} + + {!isLoading && isError && ( +

책 이야기를 불러오는 데 실패했습니다.

+ )} + + {!isLoading && !isError && stories.length === 0 && ( +

작성한 책 이야기가 없습니다.

+ )} +
- {DUMMY_MY_STORIES.map((story) => ( + {stories.map((story) => ( ))}
+ + {/* Infinite Scroll Trigger */} +
+ + {isFetchingNextPage && ( +

추가 이야기를 불러오는 중...

+ )}
); }; diff --git a/src/hooks/queries/useStoryQueries.ts b/src/hooks/queries/useStoryQueries.ts index 94c6871..e68cbeb 100644 --- a/src/hooks/queries/useStoryQueries.ts +++ b/src/hooks/queries/useStoryQueries.ts @@ -6,6 +6,7 @@ export const storyKeys = { all: ["stories"] as const, list: () => [...storyKeys.all, "list"] as const, infiniteList: () => [...storyKeys.all, "infiniteList"] as const, + myList: () => [...storyKeys.all, "myList"] as const, detail: (id: number) => [...storyKeys.all, "detail", id] as const, }; @@ -35,3 +36,15 @@ export const useInfiniteStoriesQuery = () => { }, }); }; + +export const useMyInfiniteStoriesQuery = () => { + return useInfiniteQuery({ + queryKey: storyKeys.myList(), + queryFn: ({ pageParam }) => storyService.getMyStories(pageParam ?? undefined), + initialPageParam: null as number | null, + getNextPageParam: (lastPage) => { + if (!lastPage || !lastPage.hasNext) return undefined; + return lastPage.nextCursor; + }, + }); +}; diff --git a/src/lib/api/endpoints/bookstory.ts b/src/lib/api/endpoints/bookstory.ts index 7f13100..a88bb46 100644 --- a/src/lib/api/endpoints/bookstory.ts +++ b/src/lib/api/endpoints/bookstory.ts @@ -2,4 +2,5 @@ import { API_BASE_URL } from "./base"; export const STORY_ENDPOINTS = { LIST: `${API_BASE_URL}/book-stories`, + ME: `${API_BASE_URL}/book-stories/me`, }; diff --git a/src/services/storyService.ts b/src/services/storyService.ts index 9ff936e..b0e7d35 100644 --- a/src/services/storyService.ts +++ b/src/services/storyService.ts @@ -13,6 +13,15 @@ export const storyService = { ); return response.result!; }, + getMyStories: async (cursorId?: number): Promise => { + const response = await apiClient.get>( + STORY_ENDPOINTS.ME, + { + params: { cursorId }, + } + ); + return response.result!; + }, getStoryById: async (id: number): Promise => { const response = await apiClient.get>( `${STORY_ENDPOINTS.LIST}/${id}` From acd6e47c19a48374941d49b24e5cdbef60a85a63 Mon Sep 17 00:00:00 2001 From: shinwookkang Date: Wed, 25 Feb 2026 14:34:02 +0900 Subject: [PATCH 02/10] =?UTF-8?q?=EC=97=90=EB=9F=AC=20=ED=95=B8=EB=93=A4?= =?UTF-8?q?=EB=A7=81=20=EB=B0=A9=EC=96=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../base-ui/MyPage/MyBookStoryList.tsx | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/src/components/base-ui/MyPage/MyBookStoryList.tsx b/src/components/base-ui/MyPage/MyBookStoryList.tsx index 43d93d3..6af632b 100644 --- a/src/components/base-ui/MyPage/MyBookStoryList.tsx +++ b/src/components/base-ui/MyPage/MyBookStoryList.tsx @@ -39,18 +39,18 @@ const MyBookStoryList = () => { )}
- {stories.map((story) => ( + {stories.map((story, index) => ( ))} From c602f72ac05256913cdeb07f4c90c567e3ecc66d Mon Sep 17 00:00:00 2001 From: shinwookkang Date: Wed, 25 Feb 2026 14:52:36 +0900 Subject: [PATCH 03/10] feat: integrate My Clubs API into MyMeetingList --- .../base-ui/MyPage/MyMeetingList.tsx | 35 +++++++++++++++++-- .../base-ui/MyPage/items/MyMeetingCard.tsx | 8 ++--- src/hooks/queries/useClubQueries.ts | 14 ++++++++ src/lib/api/endpoints/club.ts | 5 +++ src/services/clubService.ts | 13 +++++++ src/types/club.ts | 8 +++++ 6 files changed, 76 insertions(+), 7 deletions(-) create mode 100644 src/hooks/queries/useClubQueries.ts create mode 100644 src/lib/api/endpoints/club.ts create mode 100644 src/services/clubService.ts create mode 100644 src/types/club.ts diff --git a/src/components/base-ui/MyPage/MyMeetingList.tsx b/src/components/base-ui/MyPage/MyMeetingList.tsx index e2cdf63..d126fb4 100644 --- a/src/components/base-ui/MyPage/MyMeetingList.tsx +++ b/src/components/base-ui/MyPage/MyMeetingList.tsx @@ -1,14 +1,43 @@ "use client"; import React from "react"; -import { DUMMY_MEETINGS } from "@/constants/mocks/mypage"; import MyMeetingCard from "./items/MyMeetingCard"; +import { useMyClubsQuery } from "@/hooks/queries/useClubQueries"; const MyMeetingList = () => { + const { data, isLoading, isError } = useMyClubsQuery(); + const clubs = data?.clubList || []; + + if (isLoading) { + return ( +
+ 불러오는 중... +
+ ); + } + + if (isError) { + return ( +
+ 독서 모임을 불러오는 데 실패했습니다. +
+ ); + } + + if (clubs.length === 0) { + return ( +
+

+ 가입한 독서 모임이 없습니다. +

+
+ ); + } + return (
- {DUMMY_MEETINGS.map((meeting) => ( - + {clubs.map((club) => ( + ))}
); diff --git a/src/components/base-ui/MyPage/items/MyMeetingCard.tsx b/src/components/base-ui/MyPage/items/MyMeetingCard.tsx index 0f5c99c..adb6de6 100644 --- a/src/components/base-ui/MyPage/items/MyMeetingCard.tsx +++ b/src/components/base-ui/MyPage/items/MyMeetingCard.tsx @@ -2,17 +2,17 @@ import React from "react"; import Image from "next/image"; -import { MyPageMeeting } from "@/types/mypage"; +import { MyClubInfo } from "@/types/club"; interface MyMeetingCardProps { - meeting: MyPageMeeting; + club: MyClubInfo; } -const MyMeetingCard = ({ meeting }: MyMeetingCardProps) => { +const MyMeetingCard = ({ club }: MyMeetingCardProps) => { return (
- {meeting.title} + {club.clubName}