불러오는 중...
책장 상세 조회에 실패했습니다.
{activeTab === "meeting" && (
-
+
)}
{activeTab === "topic" && (
@@ -95,14 +220,43 @@ export default function AdminBookDetailPage() {
myName={myName}
myProfileImageUrl={myProfileImageUrl}
defaultProfileUrl="/profile4.svg"
+ isStaff={isStaff}
isWriting={isDebateWriting}
onToggleWriting={() => setIsDebateWriting((v) => !v)}
onSendDebate={(text) => {
- console.log("topic send:", { meetingId, text });
- // TODO: topic API 연결부
+ createTopic(
+ { clubId, meetingId, body: { description: text } },
+ {
+ onSuccess: () => {
+ toast.success("발제가 등록되었습니다.");
+ setIsDebateWriting(false);
+ },
+ onError: () => toast.error("발제 등록에 실패했습니다."),
+ }
+ );
return true;
}}
- items={MOCK_DEBATE_TOPICS}
+ items={topicItems as any}
+ onReport={() => reportToast()}
+ onUpdate={(id, nextContent) => {
+ updateTopic(
+ { clubId, meetingId, topicId: Number(id), body: { description: nextContent } },
+ {
+ onSuccess: () => toast.success("수정되었습니다."),
+ onError: () => toast.error("수정에 실패했습니다."),
+ }
+ );
+ }}
+ onDelete={(id) => {
+ deleteTopic(
+ { clubId, meetingId, topicId: Number(id) },
+ {
+ onSuccess: () => toast.success("삭제되었습니다."),
+ onError: () => toast.error("삭제에 실패했습니다."),
+ }
+ );
+ }}
+ onClickAuthor={handleClickAuthor}
/>
)}
@@ -111,15 +265,48 @@ export default function AdminBookDetailPage() {
myName={myName}
myProfileImageUrl={myProfileImageUrl}
defaultProfileUrl="/profile4.svg"
+ isStaff={isStaff}
isWriting={isReviewWriting}
onToggleWriting={() => setIsReviewWriting((v) => !v)}
onSendReview={(text, rating) => {
- console.log("review send:", { meetingId, text, rating });
- // TODO: review API 연결부
+ createReview(
+ { clubId, meetingId, body: { description: text, rate: rating } },
+ {
+ onSuccess: () => {
+ toast.success("한줄평이 등록되었습니다.");
+ setIsReviewWriting(false);
+ },
+ onError: () => toast.error("한줄평 등록에 실패했습니다."),
+ }
+ );
return true;
}}
- items={MOCK_REVIEWS}
- onClickMore={(id) => console.log("more:", id)}
+ items={reviewItems as any}
+ onReport={(id) => reportToast()}
+ onUpdate={(id, nextContent, nextRating) => {
+ updateReview(
+ {
+ clubId,
+ meetingId,
+ reviewId: Number(id),
+ body: { description: nextContent, rate: nextRating },
+ },
+ {
+ onSuccess: () => toast.success("수정되었습니다."),
+ onError: () => toast.error("수정에 실패했습니다."),
+ }
+ );
+ }}
+ onDelete={(id) => {
+ deleteReview(
+ { clubId, meetingId, reviewId: Number(id) },
+ {
+ onSuccess: () => toast.success("삭제되었습니다."),
+ onError: () => toast.error("삭제에 실패했습니다."),
+ }
+ );
+ }}
+ onClickAuthor={handleClickAuthor}
/>
)}
diff --git a/src/app/groups/[id]/bookcase/page.tsx b/src/app/groups/[id]/bookcase/page.tsx
index d85cf86..c676a9a 100644
--- a/src/app/groups/[id]/bookcase/page.tsx
+++ b/src/app/groups/[id]/bookcase/page.tsx
@@ -1,102 +1,156 @@
"use client";
+import React, { useEffect, useMemo, useRef } from "react";
import BookcaseCard from "@/components/base-ui/Bookcase/BookcaseCard";
import FloatingFab from "@/components/base-ui/Float";
-import { BookcaseApiResponse, groupByGeneration } from "@/types/groups/bookcasehome";
+import { BookcaseApiResponse } from "@/types/groups/bookcasehome";
import { useParams, useRouter } from "next/navigation";
-
-
-
-//API 형태 그대로
-const MOCK_BOOKCASE_RESPONSE: BookcaseApiResponse = {
- isSuccess: true,
- code: "COMMON200",
- message: "성공입니다.",
- result: {
- bookShelfInfoList: [
- {
- meetingInfo: { meetingId: 2, generation: 1, tag: "MEETING", averageRate: 3 },
- bookInfo: {
- bookId: "9791192625133",
- title: "거인의 어깨 1 - 벤저민 그레이엄, 워런 버핏, 피터 린치에게 배우다",
- author: "홍진채",
- imgUrl: null,
- },
- },
- {
- meetingInfo: { meetingId: 1, generation: 1, tag: "MEETING", averageRate: 4.5 },
- bookInfo: {
- bookId: "9791192005317",
- title: "살인자ㅇ난감",
- author: "꼬마비",
- imgUrl: null,
- },
- },
- {
- meetingInfo: { meetingId: 3, generation: 2, tag: "MEETING", averageRate: 3.8 },
- bookInfo: {
- bookId: "1",
- title: "더미 책",
- author: "더미 작가",
- imgUrl: null,
- },
- },
- ],
- hasNext: false,
- nextCursor: null,
- },
-};
+import { useClubsBookshelfSimpleInfiniteQuery } from "@/hooks/queries/useClubsBookshelfQueries";
export default function BookcasePage() {
const router = useRouter();
const params = useParams();
- const groupId = params.id as string;
-
- const sections = groupByGeneration(MOCK_BOOKCASE_RESPONSE.result.bookShelfInfoList);
+ const groupId = Number(params.id);
type TabParam = "topic" | "review" | "meeting";
const handleGoToDetail = (meetingId: number, tab: TabParam) => {
- router.push(`/groups/${groupId}/bookcase/${meetingId}?tab=${tab}`);
-};
+ router.push(`/groups/${groupId}/bookcase/${meetingId}?tab=${tab}`);
+ };
+
+ const {
+ data,
+ isLoading,
+ isError,
+ error,
+ fetchNextPage,
+ hasNextPage,
+ isFetchingNextPage,
+ } = useClubsBookshelfSimpleInfiniteQuery(groupId);
+
+ const autoFetchCountRef = useRef(0);
+ const AUTO_FETCH_LIMIT = 10;
+
+ useEffect(() => {
+ if (!hasNextPage) return;
+ if (isFetchingNextPage) return;
+ if (autoFetchCountRef.current >= AUTO_FETCH_LIMIT) return;
+
+ autoFetchCountRef.current += 1;
+ fetchNextPage();
+ }, [hasNextPage, isFetchingNextPage, fetchNextPage]);
+
+ const mergedBookShelfInfoList = useMemo(() => {
+ const pages = data?.pages ?? [];
+ return pages.flatMap((p) => p.bookShelfInfoList ?? []);
+ }, [data]);
+
+ const isStaff = useMemo(() => {
+ const first = data?.pages?.[0];
+ return Boolean(first?.isStaff);
+ }, [data]);
+
+ const adaptedResponse: BookcaseApiResponse | null = useMemo(() => {
+ if (!data?.pages?.length) return null;
+
+ const first = data.pages[0];
+
+ return {
+ isSuccess: true,
+ code: "COMMON200",
+ message: "성공입니다.",
+ result: {
+ bookShelfInfoList: mergedBookShelfInfoList,
+ hasNext: Boolean(first.hasNext),
+ nextCursor: first.nextCursor == null ? null : String(first.nextCursor),
+ },
+ };
+ }, [data, mergedBookShelfInfoList]);
+
+ if (Number.isNaN(groupId)) {
+ return (
+
+ 잘못된 모임 ID
+
+ );
+ }
+
+ if (isLoading) {
+ return (
+
+ 불러오는 중...
+
+ );
+ }
+
+ if (isError) {
+ return (
+
+ 책장 조회 실패: {(error as Error)?.message ?? "unknown error"}
+
+ );
+ }
+
+ if (!adaptedResponse || mergedBookShelfInfoList.length === 0) {
+ return (
+
+
아직 책장이 없습니다.
+
+ {isStaff && (
+
router.push(`/groups/${groupId}/admin/bookcase/new`)}
+ />
+ )}
+
+ );
+ }
+
+ const list = adaptedResponse.result.bookShelfInfoList;
return (
-
- {/* 책장 리스트 영역 */}
- {sections.map((section) => (
-
- {/* 기수 라벨 */}
-
- {section.generationLabel}
-
-
- {/* 카드 리스트 */}
-
- {section.books.map((book) => (
- handleGoToDetail(book.meetingId, "topic")}
- onReviewClick={() => handleGoToDetail(book.meetingId, "review")}
- onMeetingClick={() => handleGoToDetail(book.meetingId, "meeting")}
- />
- ))}
-
-
- ))}
-
-
router.push(`/groups/${groupId}/admin/bookcase/new`)}
- />
-
- );
-}
+
+ {list.map((item) => {
+ const meetingId = item.meetingInfo.meetingId;
+ const bookId = item.bookInfo.bookId;
+
+ return (
+ handleGoToDetail(meetingId, "topic")}
+ onReviewClick={() => handleGoToDetail(meetingId, "review")}
+ onMeetingClick={() => handleGoToDetail(meetingId, "meeting")}
+ />
+ );
+ })}
+ {isStaff && (
+ router.push(`/groups/${groupId}/admin/bookcase/new`)}
+ />
+ )}
+
+ );
+}
\ No newline at end of file
diff --git a/src/app/groups/[id]/dummy.ts b/src/app/groups/[id]/dummy.ts
deleted file mode 100644
index 9a935d1..0000000
--- a/src/app/groups/[id]/dummy.ts
+++ /dev/null
@@ -1,55 +0,0 @@
-// dummy.ts (page.tsx와 같은 폴더)
-import type { ClubHomeResponse, ClubHomeResponseResult,ClubModalLink } from '@/types/groups/grouphome';
-
-
-
-export const DUMMY_CLUB_HOME_RESPONSE: ClubHomeResponse = {
- isSuccess: true,
- code: 'COMMON200',
- message: '성공입니다.',
- result: {
- clubId: 1,
- name: '서울 독서모임',
- profileImageUrl: null,
- region: '서울',
- category: [
- { code: 'HUMANITIES', description: '인문학' },
- { code: 'COMPUTER_IT', description: '컴퓨터/IT' },
- { code: 'ESSAY', description: '에세이' },
- { code: 'HISTORY_CULTURE', description: '역사/문화' },
- { code: 'COMPUTER_IT', description: '정치/외교/국방' },
- { code: 'HISTORY_CULTURE', description: '어린이/청소년' },
- ],
- participantTypes: [
- { code: 'OFFLINE', description: '대면' },
- { code: 'WORKER', description: '직장인' },
- { code: 'STUDENT', description: '대학생' },
- ],
- open: true,
-
- description:
- '책을 좋아하는 사람들이 모여 각자의 속도로 읽고, 각자의 언어로 생각을 나누는 책 모임입니다. 정답을 찾기보다 질문을 남기는 시간을 소중히 여기며, 한 권의 책을 통해 서로의 관점과 경험을 자연스럽게 공유하는 것을 목표로 합니다.',
- recentNotice: {
- noticeId: 24,
- title: '공지사항_미리보기',
- createdAt: '2026-02-02T00:00:00.000Z',
- url: '/groups/1/notices/24',
- },
- links: {
- joinUrl: '/groups/1/join',
- contactUrl: '/contact',
- },
-
- modalLinks: [
- { id: 1, url: 'https://instagram.com/seoul_bookclub' },
- { id: 2, url: 'https://open.kakao.com/o/g0AbCDeF' },
- { id: 3, url: 'https://forms.gle/8YqZpZkQkQ2nH9rK9' },
- ],
-
- // 운영진 여부 (true: 운영진, false: 일반 회원)
- isAdmin: true,
- },
-};
-
-
-export const DUMMY_CLUB_HOME: ClubHomeResponseResult = DUMMY_CLUB_HOME_RESPONSE.result;
diff --git a/src/app/groups/[id]/layout.tsx b/src/app/groups/[id]/layout.tsx
index 77a7e43..cc1a30e 100644
--- a/src/app/groups/[id]/layout.tsx
+++ b/src/app/groups/[id]/layout.tsx
@@ -5,7 +5,7 @@ import { useParams, usePathname } from "next/navigation";
import Link from "next/link";
import Image from "next/image";
-type TabType = "home" | "notice" | "bookshelf";
+type TabType = "home" | "notice" | "bookcase";
export default function GroupDetailLayout({
children,
@@ -25,17 +25,12 @@ export default function GroupDetailLayout({
const isAdmin = pathname.includes("/admin");
const getActiveTab = (): TabType => {
- if (isAdmin) {
- if (pathname.includes("/admin/notice")) return "notice";
- if (pathname.includes("/admin/bookcase")) return "bookshelf";
- return "home";
- }
-
if (pathname.includes("/notice")) return "notice";
- if (pathname.includes("/bookcase")) return "bookshelf";
+ if (pathname.includes("/bookcase")) return "bookcase";
return "home";
};
+
const activeTab = getActiveTab();
// 더미 데이터
@@ -46,19 +41,19 @@ export default function GroupDetailLayout({
{
id: "home" as TabType,
label: "모임 홈",
- href: `/groups/${groupId}/admin`,
+ href: `/groups/${groupId}`,
icon: "/group_home.svg",
},
{
id: "notice" as TabType,
label: "공지사항",
- href: `/groups/${groupId}/admin/notice`,
+ href: `/groups/${groupId}/notice`,
icon: "/Notification2.svg",
},
{
- id: "bookshelf" as TabType,
+ id: "bookcase" as TabType,
label: "책장",
- href: `/groups/${groupId}/admin/bookcase`,
+ href: `/groups/${groupId}/bookcase`,
icon: "/bookshelf.svg",
},
]
@@ -76,7 +71,7 @@ export default function GroupDetailLayout({
icon: "/Notification2.svg",
},
{
- id: "bookshelf" as TabType,
+ id: "bookcase" as TabType,
label: "책장",
href: `/groups/${groupId}/bookcase`,
icon: "/bookshelf.svg",
@@ -94,7 +89,7 @@ export default function GroupDetailLayout({