Skip to content

Commit

Permalink
feat : 보관함 쿼리 적용 무한스크롤 설정 (#344)
Browse files Browse the repository at this point in the history
* env : react-intersection-observer

* feat : 무한스크롤 적용
  • Loading branch information
cmlim0070 authored Dec 8, 2024
1 parent 54e32ed commit c88b8e9
Show file tree
Hide file tree
Showing 6 changed files with 1,608 additions and 1,511 deletions.
2,856 changes: 1,442 additions & 1,414 deletions .pnp.cjs

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-icons": "^5.3.0",
"react-intersection-observer": "^9.13.1",
"react-map-gl": "^7.1.7",
"react-query": "^3.39.3",
"react-router-dom": "^7.0.1",
Expand Down
152 changes: 61 additions & 91 deletions src/components/StoragePage/StorageList.tsx
Original file line number Diff line number Diff line change
@@ -1,52 +1,36 @@
import React, { useEffect, useState } from 'react';
import { BottleLetter } from '../Common/BottleLetter/BottleLetter';
import { Itembox } from '../Common/Itembox/Itembox';
import { getLetter } from '@/service/storage/getLetter';
import { useNavigate } from 'react-router-dom';

interface Letter {
letterId: number;
title: string;
label: string;
letterType: string;
boxType: string;
createdAt: string;
}

interface DayGroup {
date: string;
letters: Letter[];
}
import { useInfiniteStorageFetch } from '@/hooks/useInfiniteStorageFetch';
import { useInView } from 'react-intersection-observer';

type storageType = 'keyword' | 'map' | 'bookmark';
type FilterType = 'LETTER' | 'REPLY_LETTER';
type FilterType = 'SEND' | 'RECEIVE';

type StorageListProps = {
type: storageType;
};

const ROWS_PER_PAGE = 5;

export const StorageList = ({ type = 'keyword' }: StorageListProps) => {
const navigate = useNavigate();

// const queryClient = useQueryClient();
// const { data, error, fetchNextPage, hasNextPage, isFetchNextPage } =
// useInfiniteFetch();
const page = 1;
const size = 10;
const [selectedFilter, setSelectedFilter] = useState<FilterType>('LETTER');
const [selectedFilter, setSelectedFilter] = useState<FilterType>('SEND');
const [checkedItems, setCheckedItems] = useState<number[]>([]);
const [groupedLetters, setGroupedLetters] = useState<DayGroup[]>([]);

// 리스트 타입 - 필터별 엔드포인트 추출
const getApiEndpoint = (type: storageType, filter: FilterType) => {
const { ref, inView } = useInView();

const getApiEndpoint = () => {
const endpoints = {
keyword: {
LETTER: '/letters/saved/sent',
REPLY_LETTER: '/letters/saved/received'
SEND: '/letters/saved/sent',
RECEIVE: '/letters/saved/received'
},
map: {
LETTER: '/map/sent',
REPLY_LETTER: '/map/received'
SEND: '/map/sent',
RECEIVE: '/map/received'
},
bookmark: '/map/archived'
};
Expand All @@ -55,56 +39,32 @@ export const StorageList = ({ type = 'keyword' }: StorageListProps) => {
return endpoints[type];
}

return endpoints[type]?.[filter];
return endpoints[type]?.[selectedFilter];
};

// 데이터 패치
// 따로 필터링 해줄 필요 없이 엔드포인트가 다르게 들어감
const getLetterList = async () => {
const apiEndpoint = getApiEndpoint(type, selectedFilter);
const response = await getLetter({ apiEndpoint, page, size });
console.log('응답:', response);
if (response.isSuccess) {
return response.result.content;
const renderCategory = (boxType: string, letterType: string) => {
const condition = `${boxType}-${letterType}`;
switch (condition) {
case 'SEND-LETTER':
return '보낸 편지';
case 'SEND-REPLY_LETTER':
return '보낸 답장';
case 'RECEIVE-LETTER':
return '받은 편지';
case 'RECEIVE-REPLY_LETTER':
return '받은 답장';
default:
}
return [];
};

// 편지 리스트를 날짜별로 그룹화, 날짜순으로 정렬
const groupLettersByDate = (letters: Letter[]): DayGroup[] => {
const grouped = letters.reduce(
(acc: { [key: string]: Letter[] }, letter) => {
const date = new Date(letter.createdAt)
.toISOString()
.split('T')[0];

if (!acc[date]) {
acc[date] = [];
}
acc[date].push(letter);
return acc;
},
{}
);
return Object.entries(grouped)
.map(([date, letters]) => ({
date,
letters: letters.sort(
(a, b) =>
new Date(b.createdAt).getTime() -
new Date(a.createdAt).getTime()
)
}))
.sort(
(a, b) =>
new Date(b.date).getTime() - new Date(a.date).getTime()
);
};

const setData = async () => {
const letters = await getLetterList();
setGroupedLetters(groupLettersByDate(letters));
};
const {
groupedLetters,
isLoading,
isError,
fetchNextPage,
// isFetching,
isFetchingNextPage
} = useInfiniteStorageFetch(getApiEndpoint(), ROWS_PER_PAGE);

// 체크박스 단일 클릭
const handleSingleCheck = (checked: boolean, id: number) => {
Expand All @@ -130,39 +90,43 @@ export const StorageList = ({ type = 'keyword' }: StorageListProps) => {
}
};

// 테스트 출력
useEffect(() => {
console.log(checkedItems);
}, [checkedItems]);
if (inView) {
fetchNextPage();
}
}, [inView]);

// 테스트 데이터 세팅?
useEffect(() => {
setData();
}, [type, selectedFilter]);
if (isLoading) {
return <div>로딩중</div>;
}

if (isError) {
return <></>;
}

const renderList = () => {
return (
<div className="flex flex-col gap-2 mt-2">
<div className="flex flex-col gap-2">
<div className="flex flex-row gap-2">
<button
className={`border border-sample-blue rounded-xl text-sm px-2 py-1
${
selectedFilter === 'LETTER'
selectedFilter === 'SEND'
? 'bg-sample-blue text-white'
: 'bg-white text-sample-blue'
}`}
onClick={() => setSelectedFilter('LETTER')}
onClick={() => setSelectedFilter('SEND')}
>
보낸 편지
</button>
<button
className={`border border-sample-blue rounded-xl text-sm px-2 py-1
${
selectedFilter === 'REPLY_LETTER'
selectedFilter === 'RECEIVE'
? 'bg-sample-blue text-white'
: 'bg-white text-sample-blue'
}`}
onClick={() => setSelectedFilter('REPLY_LETTER')}
onClick={() => setSelectedFilter('RECEIVE')}
>
받은 편지
</button>
Expand Down Expand Up @@ -224,7 +188,7 @@ export const StorageList = ({ type = 'keyword' }: StorageListProps) => {
type === 'map'
) {
const dataType =
selectedFilter === 'LETTER'
selectedFilter === 'SEND'
? 'sent'
: 'received';
navigate(
Expand All @@ -238,9 +202,10 @@ export const StorageList = ({ type = 'keyword' }: StorageListProps) => {
</Itembox>
<div className="flex flex-col h-full">
<div className="text-[12px] text-gray-500 mt-2">
{letter.letterType === 'LETTER'
? '보낸 편지'
: '받은 편지'}
{renderCategory(
letter.boxType,
letter.letterType
)}
</div>
<h3 className="text-sm font-bold">
{letter.title}
Expand All @@ -256,5 +221,10 @@ export const StorageList = ({ type = 'keyword' }: StorageListProps) => {
);
};

return <div className="">{renderList()}</div>;
return (
<div className="">
{renderList()}
<div>{isFetchingNextPage ? <div></div> : <div ref={ref} />}</div>
</div>
);
};
86 changes: 86 additions & 0 deletions src/hooks/useInfiniteStorageFetch.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import { useMemo } from 'react';
import { useInfiniteQuery } from '@tanstack/react-query';
import { getLetter } from '@/service/storage/getLetter';

interface Letter {
letterId: number;
title: string;
label: string;
letterType: string;
boxType: string;
createdAt: string;
}

export const useInfiniteStorageFetch = (apiEndpoint: string, size: number) => {
const getLetterList = async ({ pageParam }: { pageParam: number }) => {
const page = pageParam;
const response = await getLetter({ apiEndpoint, page, size });
return response.result;
};

const {
data,
isLoading,
isError,
fetchNextPage,
isFetching,
isFetchingNextPage
} = useInfiniteQuery({
queryKey: ['storageLetters', apiEndpoint],
queryFn: getLetterList,
initialPageParam: 1,
getNextPageParam: (lastPage) => {
if (!lastPage || lastPage.content.length < size) {
return undefined;
}
return lastPage.page + 1;
},
refetchOnMount: false,
gcTime: 1000 * 60 * 5,
staleTime: 1000 * 30
});

const groupedLetters = useMemo(() => {
if (!data?.pages) return [];

const allLetters = data.pages.flatMap((page) => page.content);

const grouped = allLetters.reduce(
(acc: { [key: string]: Letter[] }, letter) => {
const date = new Date(letter.createdAt)
.toISOString()
.split('T')[0];

if (!acc[date]) {
acc[date] = [];
}
acc[date].push(letter);
return acc;
},
{}
);

return Object.entries(grouped)
.map(([date, letters]) => ({
date,
letters: letters.sort(
(a, b) =>
new Date(b.createdAt).getTime() -
new Date(a.createdAt).getTime()
)
}))
.sort(
(a, b) =>
new Date(b.date).getTime() - new Date(a.date).getTime()
);
}, [data]);

return {
groupedLetters,
isLoading,
isError,
fetchNextPage,
isFetching,
isFetchingNextPage
};
};
10 changes: 4 additions & 6 deletions src/pages/Storage/StoragePage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ export const StoragePage = () => {

return (
<div className="">
<div className="relative flex w-full overflow-hidden text-md align-middle h-[50px] ">
<div className="relative flex w-full overflow-hidden text-md align-middle h-[50px] text-sample-black">
<div
className="absolute bottom-0 w-1/3 h-[2px] transition-transform duration-500 ease-in-out bg-sample-blue"
style={{
Expand All @@ -46,11 +46,9 @@ export const StoragePage = () => {
<span>보관한 지도 편지</span>
</div>
</div>
<Container>
<div className="flex flex-col gap-2 mt-[15px]">
<StorageList type={storageType} />
</div>
</Container>
<div className="flex flex-col gap-2 mt-[15px]">
<StorageList type={storageType} />
</div>
</div>
);
};
14 changes: 14 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -11619,6 +11619,19 @@ __metadata:
languageName: node
linkType: hard

"react-intersection-observer@npm:^9.13.1":
version: 9.13.1
resolution: "react-intersection-observer@npm:9.13.1"
peerDependencies:
react: ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
react-dom: ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
peerDependenciesMeta:
react-dom:
optional: true
checksum: 10c0/c64722db407e0ca83945bf97621ab4618fa74f3a70b528ff43988818d46feee8962bd12e70203d2d92b31030889a1f813d00e0aa3d781a7bc39fb293c277ee04
languageName: node
linkType: hard

"react-is@npm:^16.13.1":
version: 16.13.1
resolution: "react-is@npm:16.13.1"
Expand Down Expand Up @@ -13950,6 +13963,7 @@ __metadata:
react: "npm:^18.3.1"
react-dom: "npm:^18.3.1"
react-icons: "npm:^5.3.0"
react-intersection-observer: "npm:^9.13.1"
react-map-gl: "npm:^7.1.7"
react-query: "npm:^3.39.3"
react-router-dom: "npm:^7.0.1"
Expand Down

0 comments on commit c88b8e9

Please sign in to comment.