Skip to content

Commit

Permalink
[#453] [모임] 모임 목록 페이지 개선 (#475)
Browse files Browse the repository at this point in the history
* fix: api 응답과 타입 동기화

* refactor: SimpleBookGroupCard 리팩터링

- 도서 이미지 BookCover 컴포넌트 사용하도록 수정
- 스켈레톤 작성

* feat: DetailBookGroupCard 스켈레톤 컴포넌트 작성

* feat: 내가 참여한 모임 suspense 적용, GroupPage 리팩토링

* fix: group api 타입 수정, query로 인한 빌드 오류 해결

* refactor: bookgroup 파스칼 케이스로 변경

* fix: 비로그인 시 query 런타임 오류 해결

* fix: 로그인되어있을 때만 내가 참여한 모임 스켈레톤 렌더링되도록 수정

* fix: 내가 참여한 모임 리스트 하이드레이션 오류 해결

* fix: 독서모임 페이지 무한스크롤 ref 최하단으로 이동

* fix: page skeleton 적용
  • Loading branch information
gxxrxn committed Jun 17, 2024
1 parent 395e050 commit c6b1ab1
Show file tree
Hide file tree
Showing 6 changed files with 178 additions and 104 deletions.
206 changes: 128 additions & 78 deletions src/app/group/page.tsx
Original file line number Diff line number Diff line change
@@ -1,31 +1,111 @@
'use client';

import { useEffect } from 'react';
import { useInView } from 'react-intersection-observer';

import SSRSafeSuspense from '@/components/SSRSafeSuspense';
import TopHeader from '@/v1/base/TopHeader';
import SearchGroup from '@/v1/bookGroup/SearchGroup';
import SimpleBookGroupCard from '@/v1/bookGroup/SimpleBookGroupCard';
import DetailBookGroupCard from '@/v1/bookGroup/DetailBookGroupCard';
import SearchGroupInput from '@/v1/bookGroup/SearchGroup';
import SimpleBookGroupCard, {
SimpleBookGroupCardSkeleton,
} from '@/v1/bookGroup/SimpleBookGroupCard';
import DetailBookGroupCard, {
DetailBookGroupCardSkeleton,
} from '@/v1/bookGroup/DetailBookGroupCard';

import useEntireGroupsQuery from '@/queries/group/useEntireGroupsQuery';
import useMyGroupsQuery from '@/queries/group/useMyGroupQuery';
import { Skeleton, VStack } from '@chakra-ui/react';
import { useEffect } from 'react';
import { useInView } from 'react-intersection-observer';
import { useMyProfileId } from '@/queries/user/useMyProfileQuery';
import { isAuthed } from '@/utils/helpers';
import useMounted from '@/hooks/useMounted';
import Loading from '@/v1/base/Loading';

const GroupPage = () => {
const handleSearchInputClick = () => {
alert('아직 준비 중인 기능이에요.');
};

return (
<>
<TopHeader text="Group" />
<div className="flex w-full flex-col gap-[2rem]">
<SearchGroupInput onClick={handleSearchInputClick} />
<SSRSafeSuspense fallback={<PageSkeleton />}>
{isAuthed() && <MyBookGroupList />}
<EntireBookGroupList />
</SSRSafeSuspense>
</div>
{/* <Link href={'/group/create'}>
<FloatingButton position="bottom-right" />
</Link> */}
</>
);
};

export default GroupPage;

const MyBookGroupList = () => {
const {
data: { bookGroups },
} = useMyGroupsQuery({ enabled: isAuthed() });
const { data: myId } = useMyProfileId({ enabled: isAuthed() });

return (
<div className="flex gap-[1rem] overflow-scroll">
{bookGroups.map(({ title, book, bookGroupId, owner }) => (
<SimpleBookGroupCard
key={bookGroupId}
title={title}
imageSource={book.imageUrl}
isOwner={owner.id === myId}
bookGroupId={bookGroupId}
/>
))}
</div>
);
};

const PageSkeleton = () => {
const isMounted = useMounted();

if (!isMounted) {
return <Loading fullpage />;
}

return (
<>
<MyBookGroupListSkeleton />
<div className="flex flex-col gap-[1rem]">
<DetailBookGroupCardSkeleton />
<DetailBookGroupCardSkeleton />
<DetailBookGroupCardSkeleton />
<DetailBookGroupCardSkeleton />
</div>
</>
);
};

const MyBookGroupListSkeleton = () => (
<div className="flex min-h-[16.3rem] animate-pulse gap-[1rem] overflow-hidden">
<SimpleBookGroupCardSkeleton />
<SimpleBookGroupCardSkeleton />
<SimpleBookGroupCardSkeleton />
<SimpleBookGroupCardSkeleton />
</div>
);

const EntireBookGroupList = () => {
const { ref, inView } = useInView();

const {
isSuccess: entireGroupsIsSuccess,
data: entireGroupsData,
isSuccess,
data,
isLoading,
fetchNextPage,
hasNextPage,
isFetchingNextPage,
} = useEntireGroupsQuery();

const { isSuccess: myGroupsIsSuccess, data: myGroupsData } =
useMyGroupsQuery();

useEffect(() => {
if (inView && hasNextPage) {
fetchNextPage();
Expand All @@ -34,83 +114,53 @@ const GroupPage = () => {

if (isLoading)
return (
<VStack gap="0.5rem" align="stretch" w="100%">
<Skeleton height="9rem" />
<Skeleton height="28rem" />
<Skeleton height="28rem" />
<Skeleton height="28rem" />
</VStack>
<div className="flex flex-col gap-[1rem]">
<DetailBookGroupCardSkeleton />
<DetailBookGroupCardSkeleton />
<DetailBookGroupCardSkeleton />
<DetailBookGroupCardSkeleton />
</div>
);

return (
<>
<TopHeader text="Group" />
<div className="mt-[2rem] flex w-full flex-col gap-[1.5rem]">
<SearchGroup
onClick={() => {
alert('추후 업데이트 될 예정입니다.');
}}
/>
<div className="mt-[0.7rem] flex gap-[1rem] overflow-scroll">
{myGroupsIsSuccess &&
myGroupsData.bookGroups.map(group => {
const { title, book, bookGroupId } = group;
return (
//API isOwner 값이 존재하지 않아 비교하는 로직 추가 필요
<SimpleBookGroupCard
<div className="flex flex-col gap-[1rem]">
{isSuccess &&
data.pages.map(({ bookGroups }) =>
bookGroups.map(
({
bookGroupId,
title,
introduce,
book,
startDate,
endDate,
owner,
memberCount,
commentCount,
isPublic,
}) => (
<DetailBookGroupCard
key={bookGroupId}
title={title}
imageSource={book.imageUrl}
isOwner={false}
description={introduce}
bookImageSrc={book.imageUrl}
date={{ start: startDate, end: endDate }}
owner={{
name: owner.nickname,
profileImageSrc: owner.profileUrl,
}}
memberCount={memberCount}
commentCount={commentCount}
isPublic={isPublic}
bookGroupId={bookGroupId}
/>
);
})}
</div>
<div className="flex flex-col gap-[1rem]">
{entireGroupsIsSuccess &&
entireGroupsData.pages.map(groups => {
return groups.bookGroups.map(group => {
const {
title,
introduce,
book,
startDate,
endDate,
owner,
currentMemberCount,
commentCount,
isPublic,
bookGroupId,
} = group;
return (
<DetailBookGroupCard
key={bookGroupId}
title={title}
description={introduce}
bookImageSrc={book.imageUrl}
date={{ start: startDate, end: endDate }}
owner={{
name: owner.nickname,
profileImageSrc: owner.profileUrl,
}}
memberCount={currentMemberCount}
commentCount={commentCount}
isPublic={isPublic}
bookGroupId={bookGroupId}
/>
);
});
})}
</div>
)
)
)}
</div>
{isFetchingNextPage && <DetailBookGroupCardSkeleton />}
<div ref={ref} />
{isFetchingNextPage && <Skeleton w="100%" height="28rem" />}
{/* <Link href={'/group/create'}>
<FloatingButton position="bottom-right" />
</Link> */}
</>
);
};

export default GroupPage;
8 changes: 5 additions & 3 deletions src/app/profile/me/group/page.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
'use client';

import useMyGroupsQuery from '@/queries/group/useMyGroupQuery';
import { isAuthed } from '@/utils/helpers';

import BackButton from '@/v1/base/BackButton';
import TopNavigation from '@/v1/base/TopNavigation';
import DetailBookGroupCard from '@/v1/bookGroup/DetailBookGroupCard';
Expand All @@ -20,7 +22,7 @@ const UserGroupPage = () => {
};

const UserGroupContent = () => {
const { data, isSuccess } = useMyGroupsQuery();
const { data, isSuccess } = useMyGroupsQuery({ enabled: isAuthed() });

if (!isSuccess) {
return (
Expand Down Expand Up @@ -58,7 +60,7 @@ const UserGroupContent = () => {
startDate,
endDate,
owner,
currentMemberCount,
memberCount,
commentCount,
isPublic,
bookGroupId,
Expand All @@ -73,7 +75,7 @@ const UserGroupContent = () => {
name: owner.nickname,
profileImageSrc: owner.profileUrl,
}}
memberCount={currentMemberCount}
memberCount={memberCount}
commentCount={commentCount}
isPublic={isPublic}
bookGroupId={bookGroupId}
Expand Down
4 changes: 2 additions & 2 deletions src/queries/group/useMyGroupQuery.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import GroupAPI from '@/apis/group';
import { useQuery } from '@tanstack/react-query';
import useQueryWithSuspense from '@/hooks/useQueryWithSuspense';
import type { QueryOptions } from '@/types/query';
import type { APIGroupPagination } from '@/types/group';
import bookGroupKeys from './key';

const useMyGroupsQuery = (options?: QueryOptions<APIGroupPagination>) =>
useQuery(
useQueryWithSuspense(
bookGroupKeys.me(),
() => GroupAPI.getMyGroups().then(({ data }) => data),
options
Expand Down
14 changes: 7 additions & 7 deletions src/types/group.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ export interface APIGroupDetail extends APIGroup {
}

export interface APIGroupPagination extends Pagination {
bookGroups: APIGroup[];
bookGroups: (APIGroup & { memberCount: number })[];
}

export interface APICreateGroup
Expand Down Expand Up @@ -76,16 +76,16 @@ export interface APIGroupCommentPagination extends Pagination {
}

export type BookGroupDetail = {
title: APIGroup['title'];
description: APIGroup['introduce'];
title: APIGroupDetail['title'];
description: APIGroupDetail['introduce'];
bookId: APIBook['bookId'];
owner: { isMe: boolean; id: APIUser['userId'] };
date: { start: APIGroup['startDate']; end: APIGroup['endDate'] };
date: { start: APIGroupDetail['startDate']; end: APIGroupDetail['endDate'] };
memberCount: {
current: APIGroup['currentMemberCount'];
max: APIGroup['maxMemberCount'];
current: APIGroupDetail['currentMemberCount'];
max: APIGroupDetail['maxMemberCount'];
};
isPublic: APIGroup['isPublic'];
isPublic: APIGroupDetail['isPublic'];
isMember: APIGroupDetail['isGroupMember'];
};

Expand Down
23 changes: 23 additions & 0 deletions src/v1/bookGroup/DetailBookGroupCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -138,3 +138,26 @@ const CommentCount = ({ commentCount }: { commentCount: number }) => {
</div>
);
};

export const DetailBookGroupCardSkeleton = () => (
<div className="w-full animate-pulse rounded-[0.5rem] p-[1.5rem] shadow-[0_0_0.6rem_rgba(180,180,180,0.25)]">
<div className="flex gap-[0.5rem]">
<div className="h-[1.9rem] w-[4.8rem] rounded-[0.5rem] bg-black-400" />
<div className="h-[2rem] w-[3.8rem] rounded-[0.5rem] bg-black-400" />
</div>
<div className="flex justify-between gap-[1.5rem] pt-[1rem]">
<div className="flex flex-grow flex-col justify-between ">
<div className="h-[2.2rem] w-[65%] bg-black-400" />
<div className="h-[1.3rem] w-[75%] bg-black-400" />
<div className="h-[1.3rem] w-[60%] bg-black-400" />
<div className="flex w-full items-center gap-[0.5rem]">
<div className="h-[2rem] w-[2rem] rounded-full bg-black-400" />
<div className="h-[1.3rem] w-[4rem] bg-black-400" />
<div className="flex-grow" />
<div className="h-[1.3rem] w-[5rem] bg-black-400" />
</div>
</div>
<div className="h-[10.5rem] w-[7.5rem] rounded-[0.5rem] bg-black-400"></div>
</div>
</div>
);
27 changes: 13 additions & 14 deletions src/v1/bookGroup/SimpleBookGroupCard.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import Image from 'next/image';
import Link from 'next/link';
import BookCover from '../book/BookCover';

interface SimpleBookGroupCardProps {
title: string;
Expand All @@ -16,24 +16,23 @@ const SimpleBookGroupCard = ({
}: SimpleBookGroupCardProps) => {
return (
<Link href={`/group/${bookGroupId}`}>
<div className="flex w-[10rem] flex-col gap-[0.5rem]">
<div className="flex w-[10rem] flex-col gap-[1rem]">
<div className="bg-orange-100 px-[1.8rem] py-[1.6rem]">
<Image
src={imageSource}
alt="bookgroup"
width={65}
height={90}
className="rounded-[0.5rem]"
/>
</div>
<div>
<p className="break-keep text-center text-xs leading-6">
{isOwner ? `👑 ${title}` : title}
</p>
<BookCover size="xsmall" src={imageSource} />
</div>
<p className="break-keep text-center text-xs leading-tight">
{isOwner ? `👑 ${title}` : title}
</p>
</div>
</Link>
);
};

export default SimpleBookGroupCard;

export const SimpleBookGroupCardSkeleton = () => (
<div className="flex animate-pulse flex-col gap-[1rem]">
<div className="h-[12.3rem] w-[10rem] rounded-[0.5rem] bg-black-400" />
<div className="h-[1.3rem] w-[5rem] self-center bg-black-400" />
</div>
);

0 comments on commit c6b1ab1

Please sign in to comment.