Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[#453] [모임] 모임 목록 페이지 개선 #475

Merged
merged 11 commits into from
Feb 6, 2024
Merged
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('아직 준비 중인 기능이에요.');
};
Comment on lines +24 to +26
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

just comment;
다양한 클라이언트에서 일관된 경험을 제공하기 위해서 alert 을 대체할 컴포넌트가 있으면 좋긴 하겠네요.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@minjongbaek 흠.. 구현되어 있는 Toast나 Modal을 사용하면 어떨까요?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@gxxrxn 그것도 좋은 방법이네요! 그런데 다시 보니까 어차피 곧 지워질 코드로 보이네요 😅


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>
);
Loading