diff --git a/src/app/_api/list/getAllList.ts b/src/app/_api/list/getAllList.ts index 19a2c48f..f610753c 100644 --- a/src/app/_api/list/getAllList.ts +++ b/src/app/_api/list/getAllList.ts @@ -1,12 +1,18 @@ import axiosInstance from '@/lib/axios/axiosInstance'; import { AllListType } from '@/lib/types/listType'; -export async function getAllList(userId: number, type: string, category?: string) { - const query = `${category ? `${category}` : 'entire'}`; +export async function getAllList(userId: number, type: string, category: string, cursorId?: number) { + const params = new URLSearchParams({ + type, + category, + size: '5', + }); - const response = await axiosInstance.get( - `/users/${userId}/lists?type=${type}&category=${query}&size=10` - ); + if (cursorId) { + params.append('cursorId', cursorId.toString()); + } + + const response = await axiosInstance.get(`/users/${userId}/lists?${params.toString()}`); return response.data; } diff --git a/src/app/user/[userId]/_components/Card.css.ts b/src/app/user/[userId]/_components/Card.css.ts index 2efe7b71..c456b5cb 100644 --- a/src/app/user/[userId]/_components/Card.css.ts +++ b/src/app/user/[userId]/_components/Card.css.ts @@ -1,4 +1,5 @@ import { style, createVar } from '@vanilla-extract/css'; +import { vars } from '@/styles/theme.css'; export const listColor = createVar(); @@ -9,6 +10,7 @@ export const container = style({ borderRadius: '1.5rem', backgroundColor: listColor, + border: `1px solid ${vars.color.gray5}`, }); export const title = style({ @@ -16,7 +18,7 @@ export const title = style({ fontSize: '1.7rem', fontWeight: '600', - color: 'var(--text-text-grey-dark, #202020)', + color: vars.color.black, textAlign: 'right', letterSpacing: '-0.51px', wordBreak: 'keep-all', @@ -28,7 +30,7 @@ export const list = style({ fontSize: '1.2rem', fontWeight: '400', - color: 'var(--text-text-grey-dark, #202020)', + color: vars.color.black, lineHeight: '2.5rem', letterSpacing: '-0.36px', }); @@ -46,7 +48,7 @@ export const lockText = style({ fontSize: '1.1rem', fontWeight: '400', letterSpacing: '-0.33px', - color: '#AFB1B6', + color: vars.color.gray7, }); export const item = style({ @@ -58,3 +60,9 @@ export const item = style({ export const rank = style({ width: '1.2rem', }); + +export const itemTitle = style({ + overflow: 'hidden', + textOverflow: 'ellipsis', + whiteSpace: 'nowrap', +}); diff --git a/src/app/user/[userId]/_components/Card.tsx b/src/app/user/[userId]/_components/Card.tsx index f7cf5683..530c850a 100644 --- a/src/app/user/[userId]/_components/Card.tsx +++ b/src/app/user/[userId]/_components/Card.tsx @@ -39,7 +39,7 @@ export default function Card({ list, isOwner }: CardProps) { {item.ranking} {'.'} - {item.title} + {item.title} ))} diff --git a/src/app/user/[userId]/_components/Categories.css.ts b/src/app/user/[userId]/_components/Categories.css.ts index 8d07fdd1..46439b92 100644 --- a/src/app/user/[userId]/_components/Categories.css.ts +++ b/src/app/user/[userId]/_components/Categories.css.ts @@ -1,4 +1,5 @@ import { style } from '@vanilla-extract/css'; +import { vars } from '@/styles/theme.css'; export const container = style({ padding: '2.1rem 0 1.5rem 1.5rem', @@ -17,19 +18,20 @@ export const container = style({ export const button = style({ padding: '0.8rem 1.2rem', - backgroundColor: '#FFF', + backgroundColor: vars.color.white, borderRadius: '5rem', - border: '1px solid #DEDEDE', + border: `1px solid ${vars.color.gray5}`, + /** TODO - 공용폰트 body large 적용 */ fontSize: '1.6rem', - fontWeight: '500', - color: '#828282', + fontWeight: '400', + color: vars.color.gray9, letterSpacing: '-0.48px', whiteSpace: 'nowrap', }); export const variant = style({ - backgroundColor: '#0047FF', - color: '#FFF', + backgroundColor: vars.color.blue, + color: vars.color.white, border: 'none', }); diff --git a/src/app/user/[userId]/_components/Categories.tsx b/src/app/user/[userId]/_components/Categories.tsx index e55ff868..168b52b7 100644 --- a/src/app/user/[userId]/_components/Categories.tsx +++ b/src/app/user/[userId]/_components/Categories.tsx @@ -5,7 +5,6 @@ - [ ] 클릭했을때 로직 (상위요소에 핸들러 고민) (리팩토링) */ -import { useState } from 'react'; import { useQuery } from '@tanstack/react-query'; import * as styles from './Categories.css'; @@ -16,13 +15,10 @@ import { QUERY_KEYS } from '@/lib/constants/queryKeys'; interface CategoriesProps { handleFetchListsOnCategory: (category: string) => void; + selectedCategory: string; } -export const DEFAULT_CATEGORY = 'entire'; - -export default function Categories({ handleFetchListsOnCategory }: CategoriesProps) { - const [selected, setSelected] = useState(DEFAULT_CATEGORY); - +export default function Categories({ handleFetchListsOnCategory, selectedCategory }: CategoriesProps) { const { data } = useQuery({ queryKey: [QUERY_KEYS.getCategories], queryFn: getCategories, @@ -30,7 +26,6 @@ export default function Categories({ handleFetchListsOnCategory }: CategoriesPro const handleChangeCategory = (category: string) => () => { handleFetchListsOnCategory(category); - setSelected(category); }; return ( @@ -39,7 +34,7 @@ export default function Categories({ handleFetchListsOnCategory }: CategoriesPro diff --git a/src/app/user/[userId]/_components/Content.css.ts b/src/app/user/[userId]/_components/Content.css.ts index 3bbe3a9f..7da2db1c 100644 --- a/src/app/user/[userId]/_components/Content.css.ts +++ b/src/app/user/[userId]/_components/Content.css.ts @@ -1,4 +1,5 @@ import { style, styleVariants } from '@vanilla-extract/css'; +import { vars } from '@/styles/theme.css'; export const container = style({ width: '100%', @@ -7,7 +8,7 @@ export const container = style({ position: 'absolute', top: 0, - backgroundColor: '#FFF', + backgroundColor: vars.color.white, borderTopLeftRadius: '2.5rem', borderTopRightRadius: '2.5rem', }); @@ -27,10 +28,11 @@ export const button = style({ width: '100%', height: '100%', - backgroundColor: 'white', + backgroundColor: vars.color.white, borderTop: '1px solid rgba(0, 0, 0, 0.25)', borderBottom: '1px solid rgba(0, 0, 0, 0.10)', + /** TODO - 공용폰트 body large 적용 */ fontSize: '1.6rem', fontWeight: '500', }); @@ -69,3 +71,7 @@ export const variantLine = styleVariants({ }, ], }); + +export const target = style({ + height: '1px', +}); diff --git a/src/app/user/[userId]/_components/Content.tsx b/src/app/user/[userId]/_components/Content.tsx index c4cda1d4..94703bd8 100644 --- a/src/app/user/[userId]/_components/Content.tsx +++ b/src/app/user/[userId]/_components/Content.tsx @@ -2,13 +2,12 @@ /** TODO - - [ ] 무한스크롤 적용 - [ ] 피드페이지 스켈레톤 ui 적용 */ import Link from 'next/link'; -import { useCallback, useEffect, useState } from 'react'; -import { useQuery } from '@tanstack/react-query'; +import { useEffect, useMemo, useState } from 'react'; +import { useInfiniteQuery, useQuery, useQueryClient } from '@tanstack/react-query'; import { MasonryGrid } from '@egjs/react-grid'; import * as styles from './Content.css'; @@ -23,42 +22,67 @@ import { getAllList } from '@/app/_api/list/getAllList'; import { QUERY_KEYS } from '@/lib/constants/queryKeys'; import { UserType } from '@/lib/types/userProfileType'; -import { ListType } from '@/lib/types/listType'; +import { AllListType } from '@/lib/types/listType'; + +import useIntersectionObserver from '@/hooks/useIntersectionObserver'; interface ContentProps { userId: number; type: string; } +const DEFAULT_CATEGORY = 'entire'; + export default function Content({ userId, type }: ContentProps) { - const [listGrid, setListGrid] = useState([]); + const queryClient = useQueryClient(); + const [selectedCategory, setSelectedCategory] = useState(DEFAULT_CATEGORY); const { data: userData } = useQuery({ queryKey: [QUERY_KEYS.userOne, userId], queryFn: () => getUserOne(userId), }); - /** 무한스크롤시 리액트 쿼리로 불러오는게 더 나을지에 대한 고민 때문에 주석처리 해 놓은 코드 */ - // const { data: listData, refetch } = useQuery({ - // queryKey: [QUERY_KEYS.getAllList], - // queryFn: () => getAllList(userId, type), - // }); - - const handleFetchLists = useCallback( - async (category?: string) => { - const data = await getAllList(userId, type, category); - setListGrid(data.feedLists); + const { + data: listsData, + hasNextPage, + fetchNextPage, + isFetching, + } = useInfiniteQuery({ + queryKey: [QUERY_KEYS.getAllList, userId, type, selectedCategory], + queryFn: ({ pageParam: cursorId }) => { + return getAllList(userId, type, selectedCategory, cursorId as number); }, - [userId, type] - ); + initialPageParam: null, + getNextPageParam: (lastPage) => (lastPage.hasNext ? lastPage.cursorId : null), + }); - const handleFetchListsOnCategory = async (category: string) => { - handleFetchLists(category); + const lists = useMemo(() => { + return listsData ? listsData.pages.flatMap(({ feedLists }) => feedLists) : []; + }, [listsData]); + + const ref = useIntersectionObserver(() => { + if (hasNextPage) { + fetchNextPage(); + } + }); + + const handleFetchListsOnCategory = (category: string) => { + setSelectedCategory(category); + + queryClient.resetQueries({ + queryKey: [QUERY_KEYS.getAllList, userId, type, selectedCategory], + exact: true, + }); }; useEffect(() => { - handleFetchLists(); - }, [handleFetchLists]); + return () => { + queryClient.removeQueries({ + queryKey: [QUERY_KEYS.getAllList, userId, type, selectedCategory], + exact: true, + }); + }; + }, []); return (
@@ -75,15 +99,16 @@ export default function Content({ userId, type }: ContentProps) { ) : ( )} - - +
- {listGrid.map((list: ListType) => ( + {lists.map((list) => ( ))}
+ {isFetching &&
로딩중
} +
); } diff --git a/src/app/user/[userId]/_components/FollowButton.css.ts b/src/app/user/[userId]/_components/FollowButton.css.ts index fad5fe4f..8433d050 100644 --- a/src/app/user/[userId]/_components/FollowButton.css.ts +++ b/src/app/user/[userId]/_components/FollowButton.css.ts @@ -1,12 +1,13 @@ import { style } from '@vanilla-extract/css'; +import { vars } from '@/styles/theme.css'; export const button = style({ padding: '0.8rem 1.2rem', - backgroundColor: 'var(--Blue, #0047FF)', + backgroundColor: vars.color.blue, borderRadius: '5rem', fontSize: '1rem', fontWeight: '600', - color: '#fff', + color: vars.color.white, }); diff --git a/src/app/user/[userId]/_components/Profile.css.ts b/src/app/user/[userId]/_components/Profile.css.ts index f477e2e8..21dc416d 100644 --- a/src/app/user/[userId]/_components/Profile.css.ts +++ b/src/app/user/[userId]/_components/Profile.css.ts @@ -1,4 +1,5 @@ import { style, createVar } from '@vanilla-extract/css'; +import { vars } from '@/styles/theme.css'; export const imageUrl = createVar(); @@ -48,7 +49,7 @@ export const profileImage = style({ height: '5rem', borderRadius: '50%', - border: '2px solid #FFF', + border: `2px solid ${vars.color.white}`, }); export const info = style({ @@ -66,7 +67,7 @@ export const user = style({ export const nickName = style({ fontSize: '2rem', fontWeight: ' 700', - color: '#202020', + color: vars.color.black, letterSpacing: '-0.6px', }); @@ -99,7 +100,7 @@ export const description = style({ fontSize: '1.2rem', fontWeight: '500', - color: '#333', + color: vars.color.black, lineHeight: '1.6rem', letterSpacing: '-0.36px', }); diff --git a/src/hooks/useIntersectionObserver.ts b/src/hooks/useIntersectionObserver.ts new file mode 100644 index 00000000..00cfe1e5 --- /dev/null +++ b/src/hooks/useIntersectionObserver.ts @@ -0,0 +1,42 @@ +import { useEffect, useRef } from 'react'; + +/** + * Observer API를 사용하여 요소를 감지하는 커스텀 hook 입니다. + * 무한스크롤에서 사용할 수 있습니다. + * + * @param {function} callback intersection이 발생했을 때 호툴되는 콜백함수 + * @returns target 요소에 전달할 ref 값 + */ + +const options = { + root: null, + rootMain: '0px', + threshold: 1, // 단계별 콜백함수 호출 +}; + +const useIntersectionObserver = (callback: (entry: IntersectionObserverEntry) => void) => { + const ref = useRef(null); + + useEffect(() => { + if (!ref.current) return; + + const observer = new IntersectionObserver((entries) => { + if (entries[0].isIntersecting) { + callback(entries[0]); + } + }, options); + + if (ref.current) { + observer.observe(ref.current); + } + + // clean up + return () => { + observer.disconnect(); + }; + }, [ref, callback]); + + return ref; +}; + +export default useIntersectionObserver;