diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md
index c316a5a..d8bf6aa 100644
--- a/.github/pull_request_template.md
+++ b/.github/pull_request_template.md
@@ -1,9 +1,8 @@
## 요구사항
-
이슈번호: [Jira 이슈번호](Jira 이슈링크)
## 작업내용
-
-
## 테스트 결과 또는 방법
+-
diff --git a/deploy.sh b/deploy.sh
index 261b8e7..214f309 100644
--- a/deploy.sh
+++ b/deploy.sh
@@ -3,6 +3,4 @@
echo "> FE 배포"
cd /home/ubuntu/fridge-link-deploy
-pm2 restart all
-
-
+pm2 restart all
\ No newline at end of file
diff --git a/next.config.js b/next.config.js
index 408b379..04ccec9 100644
--- a/next.config.js
+++ b/next.config.js
@@ -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 {
diff --git a/src/components/organisms/FriendListItem.tsx b/src/components/organisms/FriendListItem.tsx
index e34ebcd..0411bf9 100644
--- a/src/components/organisms/FriendListItem.tsx
+++ b/src/components/organisms/FriendListItem.tsx
@@ -1,17 +1,29 @@
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 (
-
-
+
+
{name}
-
{count}
+
+ 냉장고 식자재 목록 {count}개
+
= ({
height={16}
fill="#CCCFD7"
transform="rotate(180)"
+ className="z-0"
/>
);
diff --git a/src/components/templates/FriendListTemplate.tsx b/src/components/templates/FriendListTemplate.tsx
index e6334c6..716734e 100644
--- a/src/components/templates/FriendListTemplate.tsx
+++ b/src/components/templates/FriendListTemplate.tsx
@@ -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
(null);
const { isOpen, onOpen, onClose } = useDisclosure();
const [curSortType, setCurSortType] = useState(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 ;
+ }
return (
- <>
-
+
+
-
총 {MOCK_DATA.count}명
+
+ 총 {friendsData?.pages[0].totalElements}명
+
-
- {MOCK_DATA.data.map((ele) => (
-
- ))}
+
+ {friendsData.pages.map((page) =>
+ page.content.map((ele: FriendshipData) => (
+
+ )),
+ )}
+ {isFetchingfriendsNextPage ?
:
}
{
- >
+
);
};
diff --git a/src/hooks/queries/friendship/index.ts b/src/hooks/queries/friendship/index.ts
new file mode 100644
index 0000000..035f555
--- /dev/null
+++ b/src/hooks/queries/friendship/index.ts
@@ -0,0 +1 @@
+export { default as useGetFriendships } from './useGetFriendships';
diff --git a/src/hooks/queries/friendship/useGetFriendships.ts b/src/hooks/queries/friendship/useGetFriendships.ts
new file mode 100644
index 0000000..d3aa89e
--- /dev/null
+++ b/src/hooks/queries/friendship/useGetFriendships.ts
@@ -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
({
+ queryKey: queryKeys.FRIENDSHIPS(sort),
+ url: '/friendship',
+ sort,
+ });
+};
+
+export default useGetFriendships;
diff --git a/src/hooks/queries/queryKeys.ts b/src/hooks/queries/queryKeys.ts
index 42585d8..e88a097 100644
--- a/src/hooks/queries/queryKeys.ts
+++ b/src/hooks/queries/queryKeys.ts
@@ -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];
diff --git a/src/hooks/queries/useBaseInfiniteQuery.ts b/src/hooks/queries/useBaseInfiniteQuery.ts
new file mode 100644
index 0000000..6f9bf3d
--- /dev/null
+++ b/src/hooks/queries/useBaseInfiniteQuery.ts
@@ -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 = (data: InfiniteQueryResult) => {
+ return data.last ? undefined : data.pageable.pageNumber + 1;
+};
+
+export const useBaseInfiniteQuery = ({
+ queryKey,
+ url,
+ size,
+ sort,
+}: {
+ queryKey: QueryKey;
+ url: string;
+ size?: number;
+ sort?: string;
+}) => {
+ const INITIAL_PAGE_PARAM = 0;
+ const DEFAULT_SIZE = 10;
+
+ const fetchData = async (
+ context: QueryFunctionContext,
+ ) => {
+ const { pageParam = 0 } = context;
+
+ const queryParamString = buildQuery({
+ page: pageParam,
+ size: size ?? DEFAULT_SIZE,
+ sort,
+ });
+
+ const URL = `${url}?${queryParamString}`;
+ const response =
+ await axiosInstance.get>>(URL);
+ return response.data.data;
+ };
+
+ return useInfiniteQuery({
+ queryKey,
+ queryFn: async (context: QueryFunctionContext) =>
+ await fetchData(context),
+ initialPageParam: INITIAL_PAGE_PARAM,
+ getNextPageParam: (res) => getNextOffset(res),
+ });
+};
diff --git a/src/hooks/useObserver.ts b/src/hooks/useObserver.ts
new file mode 100644
index 0000000..0264f25
--- /dev/null
+++ b/src/hooks/useObserver.ts
@@ -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;
+ 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]);
+};
diff --git a/src/pages/mypage/friendslist/index.tsx b/src/pages/mypage/friendship/index.tsx
similarity index 87%
rename from src/pages/mypage/friendslist/index.tsx
rename to src/pages/mypage/friendship/index.tsx
index 38bc936..139aadf 100644
--- a/src/pages/mypage/friendslist/index.tsx
+++ b/src/pages/mypage/friendship/index.tsx
@@ -1,10 +1,10 @@
-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[] = [
@@ -12,7 +12,7 @@ const TABS: TabLabel[] = [
{ label: '친구 추가', value: 'add' },
];
-const FriendsListPage: NextPage = () => {
+const FriendShipPage: NextPage = () => {
const [curTab, setCurTab] = useState(TABS[0]);
return (
@@ -41,4 +41,4 @@ const FriendsListPage: NextPage = () => {
);
};
-export default FriendsListPage;
+export default FriendShipPage;
diff --git a/src/pages/mypage/index.tsx b/src/pages/mypage/index.tsx
index e4fcbe9..a44407e 100644
--- a/src/pages/mypage/index.tsx
+++ b/src/pages/mypage/index.tsx
@@ -31,7 +31,7 @@ const GENERAGE_NAV_LIST = [
{
name: '친구',
svgComponent:
,
- linkTo: '/mypage/friendslist',
+ linkTo: '/mypage/friendship',
},
{ name: '나눔 내역', svgComponent:
, linkTo: '' },
];
diff --git a/src/types/common/index.d.ts b/src/types/common/index.d.ts
index 3c13eef..66633ba 100644
--- a/src/types/common/index.d.ts
+++ b/src/types/common/index.d.ts
@@ -14,3 +14,5 @@ export interface ResErrorType {
error: string;
path: string;
}
+
+export type ProfileEnum = 'GREEN' | 'RED' | 'BLUE' | 'YELLOW';
diff --git a/src/types/friendship/index.d.ts b/src/types/friendship/index.d.ts
new file mode 100644
index 0000000..da634f8
--- /dev/null
+++ b/src/types/friendship/index.d.ts
@@ -0,0 +1,7 @@
+export interface FriendshipData {
+ userId: number;
+ nickname: string;
+ ingredientCount: number;
+}
+
+export type FriendshipSortType = 'nickname' | 'createdAt';
diff --git a/src/types/query/index.d.ts b/src/types/query/index.d.ts
new file mode 100644
index 0000000..6e91493
--- /dev/null
+++ b/src/types/query/index.d.ts
@@ -0,0 +1,33 @@
+export interface InfiniteQueryResult
{
+ 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 {
+ message: string;
+ data: T;
+}
diff --git a/src/utils/buildQuery.ts b/src/utils/buildQuery.ts
new file mode 100644
index 0000000..f0971d4
--- /dev/null
+++ b/src/utils/buildQuery.ts
@@ -0,0 +1,8 @@
+export const buildQuery = (param: Record) => {
+ const queryParam = Object.fromEntries(
+ Object.entries(param).filter(
+ ([, value]) => value !== '' && value !== null && value !== undefined,
+ ),
+ );
+ return new URLSearchParams(queryParam).toString();
+};
diff --git a/src/utils/returnProfileImg.ts b/src/utils/returnProfileImg.ts
new file mode 100644
index 0000000..abd8d45
--- /dev/null
+++ b/src/utils/returnProfileImg.ts
@@ -0,0 +1,16 @@
+import type { ProfileEnum } from '@/types/common';
+
+export const returnProfileImg = (imgEnum: ProfileEnum) => {
+ switch (imgEnum) {
+ case 'GREEN':
+ return 'https://fridge-link-img.s3.ap-northeast-2.amazonaws.com/profile_img_green.png';
+ case 'RED':
+ return 'https://fridge-link-img.s3.ap-northeast-2.amazonaws.com/profile_img_red.png';
+ case 'BLUE':
+ return 'https://fridge-link-img.s3.ap-northeast-2.amazonaws.com/profile_img_blue.png';
+ case 'YELLOW':
+ return 'https://fridge-link-img.s3.ap-northeast-2.amazonaws.com/profile_img_yellow.png';
+ default:
+ return 'https://fridge-link-img.s3.ap-northeast-2.amazonaws.com/profile_img_green.png';
+ }
+};