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 : 포즈피드 무한스크롤 #44

Merged
merged 6 commits into from
Sep 7, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 8 additions & 3 deletions src/apis/apis.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,16 @@ export const getPoseDetail = (poseId: number) =>

export const getPoseTalk = () => publicApi.get<PoseTalkResponse>('/pose/talk');

export const getPoseFeed = (peopleCount: number, frameCount: number, tags: string) =>
publicApi.get<PoseFeedResponse>(`/pose`, {
export const getPoseFeed = async (
peopleCount: number,
frameCount: number,
tags: string,
pageNumber: number
) =>
await publicApi.get<PoseFeedResponse>(`/pose`, {
params: {
frameCount,
pageNumber: 0,
pageNumber,
peopleCount,
tags,
},
Expand Down
23 changes: 19 additions & 4 deletions src/apis/queries.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
import { UseQueryOptions, useQuery } from '@tanstack/react-query';
import {
UseInfiniteQueryOptions,
UseQueryOptions,
useInfiniteQuery,
useQuery,
} from '@tanstack/react-query';

import {
FilterTagsResponse,
Expand Down Expand Up @@ -30,12 +35,22 @@ export const usePoseTalkQuery = (options?: UseQueryOptions<PoseTalkResponse>) =>

export const usePoseFeedQuery = (
{ peopleCount, frameCount, tags }: FilterState,
options?: UseQueryOptions<PoseFeedResponse>
options?: UseInfiniteQueryOptions<PoseFeedResponse>
) =>
useQuery<PoseFeedResponse>(
useInfiniteQuery<PoseFeedResponse>(
['poseFeed', peopleCount, frameCount, tags],
() => getPoseFeed(peopleCount, frameCount, tags.join(',')),
({ pageParam = '' }) => getPoseFeed(peopleCount, frameCount, tags.join(','), pageParam),
{
getNextPageParam: (lastPage) => {
let target = lastPage.filteredContents;
if (lastPage.recommendation) {
target = lastPage.recommendedContents;
} else {
target = lastPage.filteredContents;
}
if (target.last) return false;
return target.number + 1;
},
...options,
}
);
Expand Down
4 changes: 2 additions & 2 deletions src/app/(Main)/feed/components/PhotoList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ interface PhotoList {

export default function PhotoList({ data }: PhotoList) {
return (
<div className="columns-2 py-16">
<>
{data ? (
data.map((item) => (
<Photo
Expand All @@ -20,6 +20,6 @@ export default function PhotoList({ data }: PhotoList) {
) : (
<Photo />
)}
</div>
</>
);
}
35 changes: 28 additions & 7 deletions src/app/(Main)/feed/page.tsx
Original file line number Diff line number Diff line change
@@ -1,24 +1,25 @@
'use client';

import Link from 'next/link';
import { useRouter, useSearchParams } from 'next/navigation';
import { useCallback } from 'react';

import EmptyCase from './components/EmptyCase';
import FilterSheet from './components/FilterSheet';
import FilterTab from './components/FilterTab';
import PhotoList from './components/PhotoList';
import { usePoseFeedQuery } from '@/apis';
import { Spacing } from '@/components/Spacing';
import { URL } from '@/constants/url';
import useDidMount from '@/hooks/useDidMount';
import useFilterState from '@/hooks/useFilterState';
import { URL } from '@/constants/url';
import useIntersect from '@/hooks/useObserver';

export default function Feed() {
const params = useSearchParams();
const router = useRouter();

const { filterState, updateFilterState } = useFilterState();
const { data, isFetched } = usePoseFeedQuery(filterState);
const { data, fetchNextPage, hasNextPage, isLoading } = usePoseFeedQuery(filterState);

useDidMount(() => {
if (!params.get('filter')) return;
Expand All @@ -30,25 +31,45 @@ export default function Feed() {
router.replace('/feed');
});

const onIntersect = useCallback(async () => {
if (hasNextPage && !isLoading) {
await fetchNextPage();
}
}, [fetchNextPage, hasNextPage, isLoading]);

const target = useIntersect(onIntersect);

return (
<>
<FilterTab />
<Spacing size={40} />
<div className="h-fit overflow-y-scroll">
{data?.recommendation && (
{data?.pages[0].recommendation ? (
<>
<EmptyCase
title={'신비한 포즈를 찾으시는군요!'}
text={'찾고 싶은 포즈를 저희에게 알려주세요.'}
button={'문의사항 남기기'}
path={URL.inquiry}
/>

<h4 className="mb-16">이런 포즈는 어때요?</h4>
<PhotoList data={data.recommendedContents.content} />
<div className="columns-2 py-16">
{data?.pages.map((page) => (
<PhotoList
key={page.recommendedContents.number}
data={page.recommendedContents.content}
/>
))}
</div>
</>
) : (
<div className="columns-2 py-16">
{data?.pages.map((page) => (
<PhotoList key={page.filteredContents.number} data={page.filteredContents.content} />
))}
</div>
)}
{isFetched ? <PhotoList data={data?.filteredContents.content} /> : <PhotoList />}
<div ref={target} className="h-1" />
</div>
<FilterSheet />
</>
Expand Down
3 changes: 1 addition & 2 deletions src/app/(Main)/pick/components/PickSection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ export default function PickSection() {
src={image || '/images/image-frame.png'}
fill
priority
className={clsx({ hidden: isLoading }, 'cursor-pointer')}
className={clsx({ hidden: isLoading }, 'cursor-pointer object-contain')}
onClick={() =>
open(({ exit }) => (
<Popup>
Expand All @@ -65,7 +65,6 @@ export default function PickSection() {
</Popup>
))
}
style={{ objectFit: 'contain' }}
alt="이미지"
/>
</div>
Expand Down
10 changes: 6 additions & 4 deletions src/app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -14,16 +14,18 @@
display: none;
}

html {
font-size: calc(16vw / 440 * 100);
}

@media (min-width: 440px) {
html {
font-size: 16px;
}
}

@supports (-webkit-touch-callout: none) {
.h-screen {
height: -webkit-fill-available;
}
}

h1 {
font-weight: 700;
font-size: 3rem;
Expand Down
5 changes: 4 additions & 1 deletion src/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ const DEFAULT_OG_IMAGE = '/images/main_star.png';
export const metadata: Metadata = {
metadataBase: new URL(BASE_SITE_URL),
title: {
template: `${DEFAULT_OG_TITLE} / %s `, // 요건 뭔가용?
template: DEFAULT_OG_TITLE,
default: DEFAULT_OG_TITLE,
},
description: DEFAULT_OG_DESC,
Expand All @@ -39,6 +39,9 @@ export const metadata: Metadata = {
maximumScale: 1,
userScalable: false,
},
icons: {
icon: './favicon.ico',
},
manifest: '/manifest.json',
themeColor: '#ffffff',
};
Expand Down
16 changes: 16 additions & 0 deletions src/hooks/useObserver.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { useEffect, useRef } from 'react';

const useIntersect = (callback: () => void, options?: IntersectionObserverInit) => {
const target = useRef(null);

useEffect(() => {
if (!target.current) return;
const observer = new IntersectionObserver(callback, options);
observer.observe(target.current);
return () => observer.disconnect();
}, [callback, options]);

return target;
};

export default useIntersect;