Skip to content

Commit

Permalink
feat: 마이페이지 친구 목록 조회 API 연동 (#26)
Browse files Browse the repository at this point in the history
* fix: mypage> friendslist -> friendship 네이밍 변경

* chore: buildQuery function 추가

* feat: useBaseInfiniteQuery hook 생성

* feat: 친구 목록 조회 API 연동

* feat: useObserver hook 추가, 무한 스크롤 적용

* docs: pr template 수정, deploy.sh 공백 제거

* feat: returnProfileImg function 생성

* feat: next.config.js image url setting

* fix: 친구 목록 프로필 이미지 적용, isFetchingNextPage 에 따라 로딩스피너 추가
  • Loading branch information
hyeseon-han authored Feb 22, 2024
1 parent 6fb9b42 commit 7ca37c5
Show file tree
Hide file tree
Showing 17 changed files with 255 additions and 50 deletions.
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
6 changes: 6 additions & 0 deletions next.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,12 @@ const nextConfig = {
reactStrictMode: true,
images: {
domains: ['mara-s3bucket.s3.ap-northeast-2.amazonaws.com'],
remotePatterns: [
{
protocol: 'https',
hostname: 'fridge-link-img.s3.ap-northeast-2.amazonaws.com',
}
]
},
webpack: (config) => {
return {
Expand Down
27 changes: 20 additions & 7 deletions src/components/organisms/FriendListItem.tsx
Original file line number Diff line number Diff line change
@@ -1,24 +1,37 @@
import { AngleIcon } from '@/assets/icons';
import { type ProfileEnum } from '@/types/common';
import { returnProfileImg } from '@/utils/returnProfileImg';
import Image from 'next/image';
import React from 'react';

const FriendListItem: React.FC<{ name: string; count: number }> = ({
name,
count,
}) => {
const FriendListItem: React.FC<{
name: string;
count: number;
profileEnum: ProfileEnum;
}> = ({ name, count, profileEnum }) => {
return (
<div className="flex p-[16px] mb-[12px] justify-between items-center bg-white rounded-[12px]">
<div className="flex">
<div className="w-10 h-10 aspect-square rounded-full bg-gray3" />
<div className="flex items-center">
<Image
src={returnProfileImg(profileEnum)}
width={40}
height={40}
className="w-[40px] h-[40px] aspect-square"
alt="친구 프로필"
/>
<div className="ml-[16px]">
<p className="mb-[4px] heading4-semibold text-gray7">{name}</p>
<p className="body2-medium text-gray5">{count}</p>
<p className="body2-medium text-gray5">
냉장고 식자재 목록 {count}
</p>
</div>
</div>
<AngleIcon
width={16}
height={16}
fill="#CCCFD7"
transform="rotate(180)"
className="z-0"
/>
</div>
);
Expand Down
80 changes: 50 additions & 30 deletions src/components/templates/FriendListTemplate.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,53 +6,73 @@ 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,
isFetchingNextPage: isFetchingfriendsNextPage,
} = useGetFriendships({
sort: curSortType.value as FriendshipSortType,
});

const onIntersect: IntersectionObserverCallback = ([entry]) => {
if (entry.isIntersecting) {
void friendsNextPage();
}
};

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

if (!friendsData?.pages[0].content) {
return <SuspenseFallback />;
}

return (
<>
<div className="mt-[57px] fixed w-screen max-w-[480px]">
<div className="h-screen flex flex-col">
<div className="mt-[57px] fixed w-screen max-w-[480px] z-10">
<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-col 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}
// TODO profileEnum api res 필드값으로 대체
profileEnum={'GREEN'}
/>
)),
)}
</div>
{isFetchingfriendsNextPage ? <SuspenseFallback /> : <div ref={bottom} />}

<Modal
onClose={onClose}
Expand Down Expand Up @@ -85,7 +105,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
2 changes: 2 additions & 0 deletions src/types/common/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,5 @@ export interface ResErrorType {
error: string;
path: string;
}

export type ProfileEnum = 'GREEN' | 'RED' | 'BLUE' | 'YELLOW';
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;
}
Loading

0 comments on commit 7ca37c5

Please sign in to comment.