Skip to content
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
22 changes: 21 additions & 1 deletion src/apis/external/useGetExternalList.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useQuery } from '@tanstack/react-query';
import { useInfiniteQuery, useQuery } from '@tanstack/react-query';
import { queryKey } from '../../constants/queryKey';
import type { PaginationDto } from '../../types/common';
import type { RequestExternalListDto, ResponseExternalDto } from '../../types/external';
Expand Down Expand Up @@ -28,3 +28,23 @@ export const useGetExternalList = (teamId: string, params: PaginationDto) => {
queryFn: () => getExternalList({ teamId }, params),
});
};

export const useGetInfiniteExternalList = (teamId: string, params: PaginationDto) => {
return useInfiniteQuery({
queryKey: [queryKey.EXTERNAL_LIST, teamId, params.query],
queryFn: ({ pageParam = '-1' }) =>
getExternalList({ teamId }, { ...params, cursor: pageParam, size: 3 }), // 한 번에 불러올 데이터 개수
initialPageParam: '-1',
getNextPageParam: (lastPage: ResponseExternalDto) => {
if (lastPage.result?.hasNext) {
return lastPage.result.nextCursor;
}
return undefined;
},
select: (data) => ({
pages: data.pages.flatMap((page) => page.result?.data ?? []),
pageParams: data.pageParams,
}),
enabled: !!teamId,
});
};
22 changes: 21 additions & 1 deletion src/apis/goal/useGetGoalList.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useQuery } from '@tanstack/react-query';
import { useInfiniteQuery, useQuery } from '@tanstack/react-query';
import { queryKey } from '../../constants/queryKey';
import type { PaginationDto } from '../../types/common';
import type { RequestGoalListDto, ResponseGoalDto } from '../../types/goal';
Expand Down Expand Up @@ -27,3 +27,23 @@ export const useGetGoalList = (teamId: string, params: PaginationDto) => {
queryFn: () => getGoalList({ teamId }, params),
});
};

export const useGetInfiniteGoalList = (teamId: string, params: PaginationDto) => {
return useInfiniteQuery({
queryKey: [queryKey.GOAL_LIST, teamId, params.query],
queryFn: ({ pageParam = '-1' }) =>
getGoalList({ teamId }, { ...params, cursor: pageParam, size: 3 }), // 한 번에 불러올 데이터 개수
initialPageParam: '-1',
getNextPageParam: (lastPage: ResponseGoalDto) => {
if (lastPage.result?.hasNext) {
return lastPage.result.nextCursor;
}
return undefined;
},
select: (data) => ({
pages: data.pages.flatMap((page) => page.result?.data ?? []),
pageParams: data.pageParams,
}),
enabled: !!teamId,
});
};
22 changes: 21 additions & 1 deletion src/apis/issue/useGetIssueList.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useQuery } from '@tanstack/react-query';
import { useInfiniteQuery, useQuery } from '@tanstack/react-query';
import { queryKey } from '../../constants/queryKey';
import type { PaginationDto } from '../../types/common';
import type { RequestIssueListDto, ResponseIssueDto } from '../../types/issue';
Expand Down Expand Up @@ -27,3 +27,23 @@ export const useGetIssueList = (teamId: string, params: PaginationDto) => {
queryFn: () => getIssueList({ teamId }, params),
});
};

export const useGetInfiniteIssueList = (teamId: string, params: PaginationDto) => {
return useInfiniteQuery({
queryKey: [queryKey.ISSUE_LIST, teamId, params.query],
queryFn: ({ pageParam = '-1' }) =>
getIssueList({ teamId }, { ...params, cursor: pageParam, size: 3 }), // 한 번에 불러올 데이터 개수
initialPageParam: '-1',
getNextPageParam: (lastPage: ResponseIssueDto) => {
if (lastPage.result?.hasNext) {
return lastPage.result.nextCursor;
}
return undefined;
},
select: (data) => ({
pages: data.pages.flatMap((page) => page.result?.data ?? []),
pageParams: data.pageParams,
}),
enabled: !!teamId,
});
};
23 changes: 23 additions & 0 deletions src/components/ListView/ListViewItemSkeleton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
const ListItemSkeleton = () => {
return (
<div className="flex justify-between items-center h-[5.6rem] px-[3.2rem] -mx-[3.2rem] animate-pulse">
<div className="flex items-center gap-[0.8rem]">
<div className="bg-gray-200 w-[6rem] h-[1.8rem] ml-[2.4rem] rounded-full" />
<div className="bg-gray-200 w-[2.4rem] h-[2.4rem] rounded-md" />
<div className="bg-gray-200 w-[12rem] h-[1.8rem] rounded-full" />
</div>
<div className="flex gap-[2.4rem] items-center">
<div className="flex gap-[2.4rem] items-center">
<div className="bg-gray-200 w-[12rem] h-[1.8rem] rounded-full" />
<div className="bg-gray-200 w-[12rem] h-[1.8rem] rounded-full" />
</div>
<div className="flex gap-[0.8rem] items-center relative">
<div className="bg-gray-200 w-[2.4rem] h-[2.4rem] rounded-full z-2" />
<div className="bg-gray-200 w-[8rem] h-[1.8rem] rounded-full" />
</div>
</div>
</div>
);
};

export default ListItemSkeleton;
13 changes: 13 additions & 0 deletions src/components/ListView/ListViewItemSkeletonList.tsx

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

P3: 반복문으로 10개 미리 보여주는거 너무 좋은데요?? 저도 적용해봐야겠네요 👍

Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import ListItemSkeleton from './ListViewItemSkeleton';

const ListViewItemSkeletonList = ({ count = 10 }: { count?: number }) => {
return (
<>
{new Array(count).fill(0).map((_, idx) => (
<ListItemSkeleton key={idx} />
))}
</>
);
};

export default ListViewItemSkeletonList;
19 changes: 19 additions & 0 deletions src/components/ListView/MergeGroup.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
type ListItem = {
id: number;
};

type GroupedList<T extends ListItem> = {
key: string;
items: T[];
};

export function mergeGroups<T extends ListItem>(groups: GroupedList<T>[]): GroupedList<T>[] {
const merged: { [key: string]: T[] } = {};

for (const group of groups) {
if (!merged[group.key]) merged[group.key] = [];
merged[group.key] = [...merged[group.key], ...group.items];
}

return Object.entries(merged).map(([key, items]) => ({ key, items }));
}
55 changes: 41 additions & 14 deletions src/pages/goal/GoalHome.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { GoalItem } from '../../components/ListView/GoalItem';
import PlusIcon from '../../assets/icons/plus.svg';
import { useMemo, useState } from 'react';
import { useEffect, useMemo, useState } from 'react';
import { PRIORITY_LABELS, STATUS_LABELS, type ItemFilter } from '../../types/listItem';
import GroupTypeIcon from '../../components/ListView/GroupTypeIcon';
import { useDropdownActions, useDropdownInfo } from '../../hooks/useDropdown';
Expand All @@ -12,8 +12,12 @@ import Modal from '../../components/Modal/Modal';
import type { GroupedGoal } from '../../types/goal';
import { getSortedGrouped } from '../../utils/listGroupSortUtils';
import { useNavigate, useParams } from 'react-router-dom';
import { useGetGoalList } from '../../apis/goal/useGetGoalList';
import { useInView } from 'react-intersection-observer';
import { useGetInfiniteGoalList } from '../../apis/goal/useGetGoalList';
import { useDeleteGoals } from '../../apis/goal/useDeleteGoals';
import ListViewItemSkeletonList from '../../components/ListView/ListViewItemSkeletonList';
import { mergeGroups } from '../../components/ListView/MergeGroup';
import Server500Error from '../Server500Error';

const FILTER_OPTIONS: ItemFilter[] = ['상태', '우선순위', '담당자'] as const;

Expand Down Expand Up @@ -45,17 +49,42 @@ const GoalHome = () => {
() => ({
// 우선 기본값 설정
cursor: '-1',
size: 10,
size: 3,
query: filterToQuery(filter),
}),
[filter]
);

// isLoading, isError 로직 추가
const { data } = useGetGoalList(teamId ?? '', params);
const goalGroups = data?.result?.data ?? [];
// 데이터 불러오기
const { data, isFetchingNextPage, isLoading, isError, hasNextPage, fetchNextPage } =
useGetInfiniteGoalList(teamId ?? '', params);

// 그룹화
const goalGroups = data?.pages ?? [];
const allGoalsFlat = goalGroups.flatMap((g) => g.goals);

const rawGoalGroups = data?.pages ?? [];
const allGroups: GroupedGoal[] = rawGoalGroups.map((g) => ({
key: g.filterName,
items: g.goals,
}));

const grouped = mergeGroups(allGroups);
const sortedGrouped = getSortedGrouped(filter, grouped);
const isEmpty = grouped.every(({ items }) => items.length === 0);

// 무한스크롤 fetching
const { ref, inView } = useInView({
threshold: 0,
});

useEffect(() => {
if (inView && hasNextPage && !isFetchingNextPage) {
fetchNextPage();
}
}, [inView, isFetchingNextPage, hasNextPage, fetchNextPage]);

// 삭제 관련 상태 및 함수
const {
checkedIds: checkItems,
isAllChecked,
Expand Down Expand Up @@ -95,13 +124,9 @@ const GoalHome = () => {
);
};

const grouped: GroupedGoal[] = goalGroups.map((g) => ({
key: g.filterName,
items: g.goals,
}));

const sortedGrouped = getSortedGrouped(filter, grouped);
const isEmpty = grouped.every(({ items }) => items.length === 0);
if (isError) {
return <Server500Error />;
}

return (
<>
Expand Down Expand Up @@ -140,8 +165,9 @@ const GoalHome = () => {
<div className="flex flex-1 items-center justify-center">
<div className="font-body-r">목표를 생성하세요</div>
</div>
) : isLoading ? (
<ListViewItemSkeletonList />
) : (
/* 리스트뷰 */
<div className="flex flex-col gap-[4.8rem]">
{sortedGrouped.map(({ key, items }) =>
/* 해당 요소 존재할 때만 생성 */
Expand Down Expand Up @@ -189,6 +215,7 @@ const GoalHome = () => {
</div>
) : null
)}
<div ref={ref}>{isFetchingNextPage && <ListViewItemSkeletonList count={3} />}</div>
</div>
)}
</div>
Expand Down
57 changes: 42 additions & 15 deletions src/pages/issue/IssueHome.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import PlusIcon from '../../assets/icons/plus.svg';
import { useMemo, useState } from 'react';
import { useEffect, useMemo, useState } from 'react';
import { PRIORITY_LABELS, STATUS_LABELS, type ItemFilter } from '../../types/listItem';
import GroupTypeIcon from '../../components/ListView/GroupTypeIcon';
import { useDropdownActions, useDropdownInfo } from '../../hooks/useDropdown';
Expand All @@ -12,8 +12,12 @@ import Modal from '../../components/Modal/Modal';
import type { GroupedIssue } from '../../types/issue';
import { getSortedGrouped } from '../../utils/listGroupSortUtils';
import { useNavigate, useParams } from 'react-router-dom';
import { useGetIssueList } from '../../apis/issue/useGetIssueList';
import { useGetInfiniteIssueList } from '../../apis/issue/useGetIssueList';
import { useDeleteIssues } from '../../apis/issue/useDeleteIssues';
import { mergeGroups } from '../../components/ListView/MergeGroup';
import { useInView } from 'react-intersection-observer';
import ListViewItemSkeletonList from '../../components/ListView/ListViewItemSkeletonList';
import Server500Error from '../Server500Error';

const FILTER_OPTIONS: ItemFilter[] = ['상태', '우선순위', '담당자', '목표'] as const;

Expand Down Expand Up @@ -47,17 +51,42 @@ const IssueHome = () => {
() => ({
// 우선 기본값 설정
cursor: '-1',
size: 10,
size: 3,
query: filterToQuery(filter),
}),
[filter]
);

// isLoading, isError 로직 추가
const { data } = useGetIssueList(teamId ?? '', params);
const issueGroups = data?.result?.data ?? [];
const allIssuesFlat = issueGroups.flatMap((g) => g.issues);
// 데이터 불러오기
const { data, isFetchingNextPage, isLoading, isError, hasNextPage, fetchNextPage } =
useGetInfiniteIssueList(teamId ?? '', params);

// 그룹화
const issueGroups = data?.pages ?? [];
const allIssuesFlat = issueGroups.flatMap((i) => i.issues);

const rawIssueGroups = data?.pages ?? [];
const allGroups: GroupedIssue[] = rawIssueGroups.map((i) => ({
key: i.filterName,
items: i.issues,
}));

const grouped = mergeGroups(allGroups);
const sortedGrouped = getSortedGrouped(filter, grouped);
const isEmpty = grouped.every(({ items }) => items.length === 0);

// 무한스크롤 fetching
const { ref, inView } = useInView({
threshold: 0,
});

useEffect(() => {
if (inView && hasNextPage && !isFetchingNextPage) {
fetchNextPage();
}
}, [inView, isFetchingNextPage, hasNextPage, fetchNextPage]);

// 삭제 관련 상태 및 함수
const {
checkedIds: checkItems,
isAllChecked,
Expand Down Expand Up @@ -97,13 +126,9 @@ const IssueHome = () => {
);
};

const grouped: GroupedIssue[] = issueGroups.map((i) => ({
key: i.filterName,
items: i.issues,
}));

const sortedGrouped = getSortedGrouped(filter, grouped);
const isEmpty = grouped.every(({ items }) => items.length === 0);
if (isError) {
return <Server500Error />;
}

return (
<>
Expand Down Expand Up @@ -142,8 +167,9 @@ const IssueHome = () => {
<div className="flex flex-1 items-center justify-center">
<div className="font-body-r">목표를 생성하세요</div>
</div>
) : isLoading ? (
<ListViewItemSkeletonList />
) : (
/* 리스트뷰 */
<div className="flex flex-col gap-[4.8rem]">
{sortedGrouped.map(({ key, items }) =>
/* 해당 요소 존재할 때만 생성 */
Expand Down Expand Up @@ -191,6 +217,7 @@ const IssueHome = () => {
</div>
) : null
)}
<div ref={ref}>{isFetchingNextPage && <ListViewItemSkeletonList count={3} />}</div>
</div>
)}
</div>
Expand Down