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

Feat: 콜렉션 리스트 무한스크롤 기능 구현 및 일부 로직 수정 #268

Merged
merged 13 commits into from
Oct 29, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 9 additions & 12 deletions src/app/_api/collect/getCollection.ts
Original file line number Diff line number Diff line change
@@ -1,29 +1,26 @@
import axiosInstance from '@/lib/axios/axiosInstance';
import { CollectionType } from '@/lib/types/listType';

interface GetCollectionType {
folderId: string;
cursorId?: number;
}

interface ResponseType {
export interface CollectionListResponseType {
collectionLists: CollectionType[];
cursorId: number;
cursorId: string;
hasNext: boolean;
folderName: string;
}

async function getCollection({ folderId, cursorId }: GetCollectionType) {
const getCollection = async (folderId: string, cursorId?: string) => {
const params = new URLSearchParams({
size: '8',
size: '10',
});

if (cursorId) {
params.append('cursorId', cursorId.toString());
}

const response = await axiosInstance.get<ResponseType>(`/folder/${folderId}/collections?${params.toString()}`);

const response = await axiosInstance.get<CollectionListResponseType>(
`/folder/${folderId}/collections?${params.toString()}`
);
return response.data;
}
};

export default getCollection;
54 changes: 35 additions & 19 deletions src/app/collection/[folderId]/_components/Collections.tsx
Original file line number Diff line number Diff line change
@@ -1,31 +1,47 @@
'use client';

import { useQuery } from '@tanstack/react-query';

import * as styles from './Collections.css';

import HeaderContainer from './CollectionsHeader';
import NoData from './NoData';
import ShapeSimpleList from '@/components/ShapeSimpleList/ShapeSimpleList';
import getCollection from '@/app/_api/collect/getCollection';
import { QUERY_KEYS } from '@/lib/constants/queryKeys';

import { CollectionType } from '@/lib/types/listType';

interface CollectionsProps {
folderId: string;
collectionList: CollectionType[];
folderName: string;
isHideOption: boolean;
handleSetOn: () => void;
handleSetOnDeleteOption: () => void;
}

// TODO 무한스크롤

export default function Collections({ folderId }: CollectionsProps) {
const { data } = useQuery({
queryKey: [QUERY_KEYS.getCollection],
queryFn: () => getCollection({ folderId }),
});

export default function Collections({
collectionList,
folderName,
isHideOption,
handleSetOn,
handleSetOnDeleteOption,
}: CollectionsProps) {
return (
<ul className={styles.container}>
{data?.collectionLists.map(({ list, id }) => {
const hasImage = !!list.representativeImageUrl;
return <ShapeSimpleList list={list} hasImage={hasImage} key={id} />;
})}
</ul>
<>
<HeaderContainer
handleSetOnBottomSheet={handleSetOn}
handleSetOnDeleteOption={handleSetOnDeleteOption}
isHideOption={isHideOption}
headerTitle={folderName}
/>

{collectionList && collectionList.length > 0 ? (
<ul className={styles.container}>
{collectionList.map(({ list, id }) => {
const hasImage = !!list.representativeImageUrl;
return <ShapeSimpleList list={list} hasImage={hasImage} key={id} />;
})}
</ul>
) : (
<NoData />
)}
</>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,16 @@ import useBooleanOutput from '@/hooks/useBooleanOutput';
interface HeaderContainerProps {
handleSetOnBottomSheet: () => void;
handleSetOnDeleteOption: () => void;
isHideOption: boolean;
headerTitle: string;
}

export default function HeaderContainer({ handleSetOnBottomSheet, handleSetOnDeleteOption }: HeaderContainerProps) {
export default function HeaderContainer({
handleSetOnBottomSheet,
handleSetOnDeleteOption,
isHideOption,
headerTitle,
}: HeaderContainerProps) {
const { isOn, handleSetOn, handleSetOff } = useBooleanOutput();

const bottomSheetOptionList = [
Expand All @@ -35,7 +42,12 @@ export default function HeaderContainer({ handleSetOnBottomSheet, handleSetOnDel

return (
<>
<Header title="콜렉션" left="back" right={<RightButton />} leftClick={() => history.back()} />
<Header
title={headerTitle}
left="back"
right={!isHideOption && <RightButton />}
leftClick={() => history.back()}
/>
{isOn && <BottomSheet onClose={handleSetOff} optionList={bottomSheetOptionList} isActive />}
</>
);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { style } from '@vanilla-extract/css';
import { vars } from '@/styles/__theme.css';
import { vars } from '@/styles/theme.css';

export const container = style({
paddingTop: '2rem',
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import { useRouter } from 'next/navigation';

import * as styles from '@/app/search/_components/NoData.css';
import * as styles from './NoData.css';
import NoListImage from '/public/images/no_data_image.svg';

import { collectionLocale } from '@/app/collection/locale';
import { useLanguage } from '@/store/useLanguage';

function NoData() {
export default function NoData() {
const { language } = useLanguage();
const router = useRouter();

Expand All @@ -23,5 +24,3 @@ function NoData() {
</div>
);
}

export default NoData;
6 changes: 6 additions & 0 deletions src/app/collection/[folderId]/page.css.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,3 +32,9 @@ export const content = style([
alignItems: 'center',
},
]);

// Observer Ref
export const target = style({
width: '100%',
height: '2px',
});
50 changes: 43 additions & 7 deletions src/app/collection/[folderId]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,29 +1,29 @@
'use client';

import { useRouter } from 'next/navigation';
import { ChangeEvent, useState } from 'react';
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { ChangeEvent, useEffect, useMemo, useState } from 'react';
import { useInfiniteQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { isAxiosError } from 'axios';

import HeaderContainer from './_components/HeaderContainer';
import Collections from './_components/Collections';
import BottomSheet from '@/components/BottomSheet/ver3.0/BottomSheet';

import * as styles from './page.css';

import useBooleanOutput from '@/hooks/useBooleanOutput';
import useIntersectionObserver from '@/hooks/useIntersectionObserver';
import { useLanguage } from '@/store/useLanguage';
import toasting from '@/lib/utils/toasting';
import toastMessage from '@/lib/constants/toastMessage';
import { QUERY_KEYS } from '@/lib/constants/queryKeys';
import updateCollectionFolder from '@/app/_api/folder/updateFolder';
import deleteFolder from '@/app/_api/folder/deleteFolder';
import getCollection, { CollectionListResponseType } from '@/app/_api/collect/getCollection';

interface ParamType {
params: { folderId: string };
}

// TODO API에 FolderName 필드 추가 요청 => input value에 보여주기 & 헤더 타이틀
export default function CollectionDetailPage({ params }: ParamType) {
const folderId = params.folderId;
const { isOn, handleSetOn, handleSetOff } = useBooleanOutput();
Expand All @@ -38,11 +38,35 @@ export default function CollectionDetailPage({ params }: ParamType) {

const [value, setValue] = useState('');

// 폴더 상세(콜렉션) 조회
const {
data: listData,
hasNextPage,
fetchNextPage,
} = useInfiniteQuery<CollectionListResponseType>({
queryKey: [QUERY_KEYS.getCollection, folderId],
queryFn: ({ pageParam: cursorId }) => getCollection(folderId, cursorId as string),
initialPageParam: null,
getNextPageParam: (lastPage) => (lastPage.hasNext ? lastPage.cursorId : null),
enabled: !!folderId,
});

const lists = useMemo(() => {
return listData ? listData.pages.flatMap(({ collectionLists }) => collectionLists) : [];
}, [listData]);

const ref = useIntersectionObserver(() => {
if (hasNextPage) {
fetchNextPage();
}
});

// 폴더 수정하기 mutation
const editFolderMutation = useMutation({
mutationFn: updateCollectionFolder,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: [QUERY_KEYS.getFolders] });
queryClient.invalidateQueries({ queryKey: [QUERY_KEYS.getCollection] });
setValue('');
handleSetOff();
},
Expand Down Expand Up @@ -97,16 +121,27 @@ export default function CollectionDetailPage({ params }: ParamType) {
deleteFolderMutation.mutate(folderId);
};

useEffect(() => {
if (listData) {
setValue(listData.pages[0].folderName);
}
}, []);

return (
<section className={styles.container}>
<HeaderContainer handleSetOnBottomSheet={handleSetOn} handleSetOnDeleteOption={handleSetOnDeleteOption} />
<Collections folderId={folderId} />
<Collections
collectionList={lists}
folderName={listData?.pages[0].folderName ?? ''}
isHideOption={folderId === '0'}
handleSetOn={handleSetOn}
handleSetOnDeleteOption={handleSetOnDeleteOption}
/>
<BottomSheet isOn={isOn}>
<BottomSheet.Title>폴더 이름 바꾸기</BottomSheet.Title>
<input
autoFocus
placeholder="폴더명을 작성해 주세요"
value={value}
defaultValue={value}
onChange={handleChangeInput}
className={styles.contentInput}
/>
Expand All @@ -125,6 +160,7 @@ export default function CollectionDetailPage({ params }: ParamType) {
{['취소', '삭제']}
</BottomSheet.Button>
</BottomSheet>
<div className={styles.target} ref={ref}></div>
</section>
);
}
2 changes: 1 addition & 1 deletion src/app/collection/_[category]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { useEffect, useMemo } from 'react';
import useIntersectionObserver from '@/hooks/useIntersectionObserver';
import getCollection from '@/app/_api/collect/__getCollection';
import Top3CardSkeleton from '@/app/collection/_[category]/_components/Top3CardSkeleton';
import NoData from '@/app/collection/_[category]/_components/NoData';
import NoData from '@/app/collection/[folderId]/_components/NoData';
import { CollectionType } from '@/lib/types/listType';
import Top3Card from '@/app/collection/_[category]/_components/Top3Card';
import { categoriesLocale } from '@/app/collection/locale';
Expand Down
43 changes: 43 additions & 0 deletions src/app/collection/_components/FolderList.css.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { style } from '@vanilla-extract/css';
import { vars } from '@/styles/theme.css';
import { Label } from '@/styles/font.css';

export const folders = style({
padding: '2.4rem 4.8rem 8.3rem 4.8rem',
display: 'grid',
gridTemplateColumns: 'repeat(2, 1fr)',
gridColumnGap: 34,
gridRowGap: 24,
});

export const folder = style({
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
gap: 16,
cursor: 'pointer',

selectors: {
'&:hover': {
transform: 'translateY(-10%)',
},
},
transition: 'transform 0.2s ease',
});

export const title = style([
Label,
{
maxWidth: 130,
display: 'flex',
gap: 6,
alignItems: 'center',
color: vars.color.black,
},
]);

export const folderName = style({
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
});
47 changes: 47 additions & 0 deletions src/app/collection/_components/FolderList.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import Link from 'next/link';
import { useQuery } from '@tanstack/react-query';

import * as styles from './FolderList.css';

import FolderIcon from '@/components/icons/FolderIcon';

import getFolders, { FoldersResponseType } from '@/app/_api/folder/getFolders';
import { QUERY_KEYS } from '@/lib/constants/queryKeys';
import { FoldersType } from '@/lib/types/folderType';

interface FolderType {
folder: FoldersType;
}

function Folder({ folder }: FolderType) {
return (
<Link href={`/collection/${folder.folderId}`} key={folder.folderId} className={styles.folder}>
<FolderIcon />
<p className={styles.title}>
<span className={styles.folderName}>{folder.folderName}</span>
{folder.listCount > 0 && <span>{`(${folder.listCount})`}</span>}
</p>
</Link>
);
}

export default function FolderList() {
const { data } = useQuery<FoldersResponseType>({
queryKey: [QUERY_KEYS.getFolders],
queryFn: getFolders,
staleTime: 1000 * 60 * 5, // 5분 설정
});

const defaultFolder: FoldersType = {
folderId: 0,
folderName: '전체',
listCount: 0,
};

return (
<div className={styles.folders}>
{data?.folders &&
[defaultFolder, ...data.folders].map((folder) => <Folder key={folder.folderId} folder={folder} />)}
</div>
);
}
Loading