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
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) {
choipureum marked this conversation as resolved.
Show resolved Hide resolved
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
Loading