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: 마이페이지 친구 목록 조회 API 연동 #26

Merged
merged 9 commits into from
Feb 22, 2024
3 changes: 1 addition & 2 deletions .github/pull_request_template.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
## 요구사항

이슈번호: [Jira 이슈번호](Jira 이슈링크)

## 작업내용

-

## 테스트 결과 또는 방법
-
4 changes: 1 addition & 3 deletions deploy.sh
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,4 @@
echo "> FE 배포"

cd /home/ubuntu/fridge-link-deploy
pm2 restart all


pm2 restart all
74 changes: 45 additions & 29 deletions src/components/templates/FriendListTemplate.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,53 +6,69 @@ import {
useDisclosure,
} from '@chakra-ui/react';

import React, { useState } from 'react';
import React, { useRef, useState } from 'react';
import { type SortLabel } from '@/types/common';
import { RadioButtonField, SortButton } from '../atoms';
import { FriendListItem } from '../organisms';
import { useGetFriendships } from '@/hooks/queries/friendship';
import type { FriendshipData, FriendshipSortType } from '@/types/friendship';
import { SuspenseFallback } from '.';
import { useObserver } from '@/hooks/useObserver';

const SORT_TYPES: SortLabel[] = [
{ label: '이름순', value: 'name' },
{ label: '등록순', value: 'latest' },
{ label: '이름순', value: 'nickname' },
{ label: '등록순', value: 'createdAt' },
];
const MOCK_DATA = {
count: 13,
data: [
{
id: 1,
name: '김지수',
ingrediendNum: 14,
},
{
id: 2,
name: '김지수',
ingrediendNum: 14,
},
],
};

const FriendListTemplate: React.FC = () => {
const bottom = useRef<HTMLDivElement>(null);
const { isOpen, onOpen, onClose } = useDisclosure();
const [curSortType, setCurSortType] = useState<SortLabel>(SORT_TYPES[0]);
const { data: friendsData, fetchNextPage: friendsNextPage } =
useGetFriendships({
sort: curSortType.value as FriendshipSortType,
});

const onIntersect: IntersectionObserverCallback = ([entry]) => {
if (entry.isIntersecting) {
void friendsNextPage();
console.log('next gogo');
}
};

useObserver({
target: bottom,
onIntersect,
});

if (!friendsData?.pages[0].content) {
choipureum marked this conversation as resolved.
Show resolved Hide resolved
return <SuspenseFallback />;
}

return (
<>
<div className="h-screen flex flex-col">
<div className="mt-[57px] fixed w-screen max-w-[480px]">
<div className="h-[1px] mt-[-1px] bg-gray1" />
<div className="flex justify-between px-[20px] py-[18px] bg-white body1-medium">
<p className="body1-medium">총 {MOCK_DATA.count}명</p>
<p className="body1-medium">
총 {friendsData?.pages[0].totalElements}명
</p>
<SortButton label={curSortType.label} onClick={onOpen} />
</div>
</div>

<div className="pt-[128px] px-[20px]">
{MOCK_DATA.data.map((ele) => (
<FriendListItem
key={ele.id}
name={ele.name}
count={ele.ingrediendNum}
/>
))}
<div className="flex flex-1 overflow-y-auto pt-[128px] px-[20px]">
{friendsData.pages.map((page) =>
page.content.map((ele: FriendshipData) => (
<FriendListItem
key={ele.userId}
name={ele.nickName}
count={ele.ingredientCount}
/>
)),
)}
</div>
<div ref={bottom} />
choipureum marked this conversation as resolved.
Show resolved Hide resolved

<Modal
onClose={onClose}
Expand Down Expand Up @@ -85,7 +101,7 @@ const FriendListTemplate: React.FC = () => {
</ModalBody>
</ModalContent>
</Modal>
</>
</div>
);
};

Expand Down
1 change: 1 addition & 0 deletions src/hooks/queries/friendship/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default as useGetFriendships } from './useGetFriendships';
14 changes: 14 additions & 0 deletions src/hooks/queries/friendship/useGetFriendships.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import type { FriendshipData, FriendshipSortType } from '@/types/friendship';

import { queryKeys } from '../queryKeys';
import { useBaseInfiniteQuery } from '../useBaseInfiniteQuery';

const useGetFriendships = ({ sort }: { sort: FriendshipSortType }) => {
return useBaseInfiniteQuery<FriendshipData[]>({
queryKey: queryKeys.FRIENDSHIPS(sort),
url: '/friendship',
sort,
});
};

export default useGetFriendships;
3 changes: 3 additions & 0 deletions src/hooks/queries/queryKeys.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import type { FriendshipSortType } from '@/types/friendship';

export const queryKeys = {
INGREDIENT: (id?: number) => (id ? ['ingredient', id] : ['ingredient']),
KAKAO: () => ['kakao'],
SHARES: () => ['shares'],
FRIENDSHIPS: (sort: FriendshipSortType) => ['friendship', sort],
} as const;

export type QueryKeys = (typeof queryKeys)[keyof typeof queryKeys];
50 changes: 50 additions & 0 deletions src/hooks/queries/useBaseInfiniteQuery.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import type { ApiResponseType, InfiniteQueryResult } from '@/types/query';
import type { QueryFunctionContext, QueryKey } from '@tanstack/react-query';

import axiosInstance from '@/api/axiosInstance';
import { buildQuery } from '@/utils/buildQuery';
import { useInfiniteQuery } from '@tanstack/react-query';

export const getNextOffset = <T>(data: InfiniteQueryResult<T>) => {
return data.last ? undefined : data.pageable.pageNumber + 1;
};

export const useBaseInfiniteQuery = <T>({
queryKey,
url,
size,
sort,
}: {
queryKey: QueryKey;
url: string;
size?: number;
sort?: string;
}) => {
const INITIAL_PAGE_PARAM = 0;
const DEFAULT_SIZE = 10;

const fetchData = async <T>(
context: QueryFunctionContext<QueryKey, number>,
) => {
const { pageParam = 0 } = context;

const queryParamString = buildQuery({
page: pageParam,
size: size ?? DEFAULT_SIZE,
sort,
});

const URL = `${url}?${queryParamString}`;
const response =
await axiosInstance.get<ApiResponseType<InfiniteQueryResult<T>>>(URL);
return response.data.data;
};

return useInfiniteQuery({
queryKey,
queryFn: async (context: QueryFunctionContext<QueryKey, number>) =>
await fetchData<T>(context),
initialPageParam: INITIAL_PAGE_PARAM,
getNextPageParam: (res) => getNextOffset<T>(res),
});
};
35 changes: 35 additions & 0 deletions src/hooks/useObserver.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import type { RefObject } from 'react';
import { useEffect } from 'react';

export const useObserver = ({
target,
onIntersect,
root = null,
rootMargin = '1px',
threshold = 0,
}: {
target: RefObject<HTMLDivElement>;
onIntersect: IntersectionObserverCallback;
root?: Element | Document | null;
rootMargin?: string;
threshold?: number | number[];
}) => {
useEffect(() => {
let observer: IntersectionObserver | undefined;

if (target?.current !== null) {
observer = new IntersectionObserver(onIntersect, {
root,
rootMargin,
threshold,
});
observer.observe(target.current);
}

return () => {
if (typeof observer !== 'undefined') {
observer.disconnect();
}
};
}, [target, rootMargin, threshold, onIntersect, root]);
};
Original file line number Diff line number Diff line change
@@ -1,18 +1,18 @@
import { SettingIcon } from '@/assets/icons';
import { TabButton } from '@/components/atoms';
import Header from '@/components/organisms/Header';
import { AddFriendTemplate, FriendListTemplate } from '@/components/templates';
import { type TabLabel } from '@/types/common';

import { type NextPage } from 'next';
import Header from '@/components/organisms/Header';
import type { NextPage } from 'next';
import { SettingIcon } from '@/assets/icons';
import { TabButton } from '@/components/atoms';
import type { TabLabel } from '@/types/common';
import { useState } from 'react';

const TABS: TabLabel[] = [
{ label: '친구 목록', value: 'list' },
{ label: '친구 추가', value: 'add' },
];

const FriendsListPage: NextPage = () => {
const FriendShipPage: NextPage = () => {
const [curTab, setCurTab] = useState<TabLabel>(TABS[0]);

return (
Expand Down Expand Up @@ -41,4 +41,4 @@ const FriendsListPage: NextPage = () => {
</div>
);
};
export default FriendsListPage;
export default FriendShipPage;
2 changes: 1 addition & 1 deletion src/pages/mypage/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ const GENERAGE_NAV_LIST = [
{
name: '친구',
svgComponent: <FriendsIcon />,
linkTo: '/mypage/friendslist',
linkTo: '/mypage/friendship',
},
{ name: '나눔 내역', svgComponent: <CartIcon />, linkTo: '' },
];
Expand Down
7 changes: 7 additions & 0 deletions src/types/friendship/index.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export interface FriendshipData {
userId: number;
nickName: string;
ingredientCount: number;
}

export type FriendshipSortType = 'nickname' | 'createdAt';
33 changes: 33 additions & 0 deletions src/types/query/index.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
export interface InfiniteQueryResult<T> {
totalPages: number;
totalElements: number;
size: number;
content: T;
number: number;
sort: {
empty: boolean;
sorted: boolean;
unsorted: boolean;
};
numberOfElements: number;
pageable: {
offset: number;
sort: {
empty: boolean;
sorted: boolean;
unsorted: boolean;
};
pageSize: number;
paged: boolean;
pageNumber: number;
unpaged: boolean;
};
first: boolean;
last: boolean;
empty: boolean;
}

export interface ApiResponseType<T> {
message: string;
data: T;
}
8 changes: 8 additions & 0 deletions src/utils/buildQuery.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
export const buildQuery = (param: Record<string, any>) => {
const queryParam = Object.fromEntries(
Object.entries(param).filter(
([, value]) => value !== '' && value !== null && value !== undefined,
choipureum marked this conversation as resolved.
Show resolved Hide resolved
),
);
return new URLSearchParams(queryParam).toString();
};
Loading