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

[FE] feat: 화면 전환 시 선택된 카테고리 유지 및 스크롤 유지 #621

Merged
merged 13 commits into from
Sep 14, 2023
Merged
Show file tree
Hide file tree
Changes from 11 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
6 changes: 3 additions & 3 deletions frontend/src/components/Common/CategoryMenu/CategoryMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { Button, theme } from '@fun-eat/design-system';
import type { CSSProp } from 'styled-components';
import styled from 'styled-components';

import { useCategoryContext } from '@/hooks/context';
import { useCategoryValueContext, useCategoryActionContext } from '@/hooks/context';
import { useCategoryQuery } from '@/hooks/queries/product';
import type { CategoryVariant } from '@/types/common';

Expand All @@ -12,8 +12,8 @@ interface CategoryMenuProps {

const CategoryMenu = ({ menuVariant }: CategoryMenuProps) => {
const { data: categories } = useCategoryQuery(menuVariant);
const { categoryIds, selectCategory } = useCategoryContext();

const { categoryIds } = useCategoryValueContext();
const { selectCategory } = useCategoryActionContext();
const currentCategoryId = categoryIds[menuVariant];

return (
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/components/Layout/DefaultLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ const DefaultLayoutContainer = styled.div`
const MainWrapper = styled.main`
position: relative;
height: calc(100% - 120px);
padding: 20px;
padding: 0 20px;
overflow-x: hidden;
overflow-y: auto;
`;
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import PBProductItem from '../PBProductItem/PBProductItem';

import { MoreButton } from '@/components/Common';
import { PATH } from '@/constants/path';
import { useCategoryContext } from '@/hooks/context';
import { useCategoryValueContext } from '@/hooks/context';
import { useInfiniteProductsQuery } from '@/hooks/queries/product';
import displaySlice from '@/utils/displaySlice';

Expand All @@ -15,7 +15,7 @@ interface PBProductListProps {
}

const PBProductList = ({ isHomePage }: PBProductListProps) => {
const { categoryIds } = useCategoryContext();
const { categoryIds } = useCategoryValueContext();

const { data: pbProductListResponse } = useInfiniteProductsQuery(categoryIds.store);
const pbProducts = pbProductListResponse.pages.flatMap((page) => page.products);
Expand Down
29 changes: 19 additions & 10 deletions frontend/src/components/Product/ProductList/ProductList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ import styled from 'styled-components';
import ProductItem from '../ProductItem/ProductItem';

import { PATH } from '@/constants/path';
import { useIntersectionObserver } from '@/hooks/common';
import { useCategoryContext } from '@/hooks/context';
import { useIntersectionObserver, useScrollRestoration } from '@/hooks/common';
import { useCategoryValueContext } from '@/hooks/context';
import { useInfiniteProductsQuery } from '@/hooks/queries/product';
import type { CategoryVariant, SortOption } from '@/types/common';
import displaySlice from '@/utils/displaySlice';
Expand All @@ -20,36 +20,45 @@ interface ProductListProps {

const ProductList = ({ category, isHomePage, selectedOption }: ProductListProps) => {
const scrollRef = useRef<HTMLDivElement>(null);
const productListRef = useRef<HTMLDivElement>(null);

const { categoryIds } = useCategoryContext();
const { categoryIds } = useCategoryValueContext();

const { fetchNextPage, hasNextPage, data } = useInfiniteProductsQuery(
categoryIds[category],
selectedOption?.value ?? 'reviewCount,desc'
);
const productList = data.pages.flatMap((page) => page.products);
const productsToDisplay = displaySlice(isHomePage, productList);

useIntersectionObserver<HTMLDivElement>(fetchNextPage, scrollRef, hasNextPage);

useScrollRestoration(categoryIds[category], productListRef);

const productList = data.pages.flatMap((page) => page.products);
const productsToDisplay = displaySlice(isHomePage, productList);

return (
<>
<ProductListContainer>
<ProductListContainer ref={productListRef}>
<ProductListWrapper>
{productsToDisplay.map((product) => (
<li key={product.id}>
<Link as={RouterLink} to={`${PATH.PRODUCT_LIST}/${category}/${product.id}`}>
<ProductItem product={product} />
</Link>
</li>
))}
</ProductListContainer>
</ProductListWrapper>
<div ref={scrollRef} aria-hidden />
</>
</ProductListContainer>
);
};
export default ProductList;

const ProductListContainer = styled.ul`
const ProductListContainer = styled.div`
height: calc(100% - 150px);
overflow-y: auto;
`;

const ProductListWrapper = styled.ul`
display: flex;
flex-direction: column;

Expand Down
31 changes: 24 additions & 7 deletions frontend/src/contexts/CategoryContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,29 +12,46 @@ type CategoryIds = {
[k in CategoryVariant]: number;
};

interface CategoryState {
interface CategoryValue {
categoryIds: CategoryIds;
currentTabScroll: { [key: number]: number };
}

interface CategoryAction {
selectCategory: (menuVariant: string, categoryId: number) => void;
saveCurrentTabScroll: (categoryId: number, scrollY: number) => void;
}

export const CategoryContext = createContext<CategoryState>({
categoryIds: initialState,
selectCategory: () => {},
});
export const CategoryValueContext = createContext<CategoryValue | null>(null);
export const CategoryActionContext = createContext<CategoryAction | null>(null);

const CategoryProvider = ({ children }: PropsWithChildren) => {
const [categoryIds, setCategoryIds] = useState(initialState);
const [currentTabScroll, setCurrentTabScroll] = useState({});

const selectCategory = (menuVariant: string, categoryId: number) => {
setCategoryIds((prevCategory) => ({ ...prevCategory, [menuVariant]: categoryId }));
};

const categoryState: CategoryState = {
const saveCurrentTabScroll = (categoryId: number, scrollY: number) => {
setCurrentTabScroll((prevState) => ({ ...prevState, [categoryId]: scrollY }));
};
Comment on lines +36 to +38
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

빈 객체로 두고 카테고리마다 하나씩 저장해두는거군요 👍

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

네!


const categoryValue = {
categoryIds,
currentTabScroll,
};

const categoryAction = {
selectCategory,
saveCurrentTabScroll,
};

return <CategoryContext.Provider value={categoryState}>{children}</CategoryContext.Provider>;
return (
<CategoryActionContext.Provider value={categoryAction}>
<CategoryValueContext.Provider value={categoryValue}>{children}</CategoryValueContext.Provider>
</CategoryActionContext.Provider>
);
};

export default CategoryProvider;
1 change: 1 addition & 0 deletions frontend/src/hooks/common/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,4 @@ export { default as useImageUploader } from './useImageUploader';
export { default as useFormData } from './useFormData';
export { default as useTimeout } from './useTimeout';
export { default as useRouteChangeTracker } from './useRouteChangeTracker';
export { default as useScrollRestoration } from './useScrollRestoration';
36 changes: 36 additions & 0 deletions frontend/src/hooks/common/useScrollRestoration.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import type { RefObject } from 'react';
import { useEffect } from 'react';

import useTimeout from './useTimeout';
import { useCategoryActionContext, useCategoryValueContext } from '../context';

const useScrollRestoration = (currentCategoryId: number, ref: RefObject<HTMLElement>) => {
const { saveCurrentTabScroll } = useCategoryActionContext();
const { currentTabScroll } = useCategoryValueContext();

const handleScroll = () => {
if (!ref.current) {
return;
}
saveCurrentTabScroll(currentCategoryId, ref.current.scrollTop);
};

const [timeoutFn] = useTimeout(handleScroll, 300);

useEffect(() => {
if (!ref.current) {
return;
}

ref.current.addEventListener('scroll', timeoutFn);

const scrollY = currentTabScroll[currentCategoryId];
ref.current.scrollTo(0, scrollY);

return () => {
ref.current?.removeEventListener('scroll', timeoutFn);
};
}, [currentCategoryId]);
};

export default useScrollRestoration;
3 changes: 2 additions & 1 deletion frontend/src/hooks/context/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export { default as useCategoryContext } from './useCategoryContext';
export { default as useCategoryValueContext } from './useCategoryValueContext';
export { default as useCategoryActionContext } from './useCategoryActionContext';
export { default as useReviewFormActionContext } from './useReviewFormActionContext';
export { default as useReviewFormValueContext } from './useReviewFormValueContext';
export { default as useRecipeFormActionContext } from './useRecipeFormActionContext';
Expand Down
14 changes: 14 additions & 0 deletions frontend/src/hooks/context/useCategoryActionContext.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { useContext } from 'react';

import { CategoryActionContext } from '@/contexts/CategoryContext';

const useCategoryActionContext = () => {
const categoryAction = useContext(CategoryActionContext);
if (categoryAction === null || categoryAction === undefined) {
throw new Error('useCategoryActionContext는 Category Provider 안에서 사용해야 합니다.');
}

return categoryAction;
};

export default useCategoryActionContext;
11 changes: 0 additions & 11 deletions frontend/src/hooks/context/useCategoryContext.ts

This file was deleted.

14 changes: 14 additions & 0 deletions frontend/src/hooks/context/useCategoryValueContext.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { useContext } from 'react';

import { CategoryValueContext } from '@/contexts/CategoryContext';

const useCategoryValueContext = () => {
const categoryValue = useContext(CategoryValueContext);
if (categoryValue === null || categoryValue === undefined) {
throw new Error('useCategoryValueContext는 Category Provider 안에서 사용해야 합니다.');
}

return categoryValue;
};

export default useCategoryValueContext;
1 change: 1 addition & 0 deletions frontend/src/pages/HomePage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ const HomePage = () => {
</Suspense>
</ErrorBoundary>
</section>
<Spacing size={36} />
<ScrollButton />
</>
);
Expand Down
20 changes: 10 additions & 10 deletions frontend/src/pages/ProductListPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,23 +23,19 @@ import { isCategoryVariant } from '@/types/common';
const PAGE_TITLE = { food: '공통 상품', store: 'PB 상품' };

const ProductListPage = () => {
const { category } = useParams();

const { ref, isClosing, handleOpenBottomSheet, handleCloseBottomSheet } = useBottomSheet();
const { selectedOption, selectSortOption } = useSortOption(PRODUCT_SORT_OPTIONS[0]);
const { reset } = useQueryErrorResetBoundary();

const { category } = useParams();

if (!category) {
return null;
}

if (!isCategoryVariant(category)) {
if (!category || !isCategoryVariant(category)) {
return null;
}

return (
<>
<section>
<ProductListSection>
<Title
headingTitle={PAGE_TITLE[category]}
routeDestination={PATH.PRODUCT_LIST + '/' + (category === 'store' ? 'food' : 'store')}
Expand All @@ -56,7 +52,7 @@ const ProductListPage = () => {
<ProductList category={category} selectedOption={selectedOption} />
</Suspense>
</ErrorBoundary>
</section>
</ProductListSection>
<ScrollButton />
<BottomSheet ref={ref} isClosing={isClosing} maxWidth="600px" close={handleCloseBottomSheet}>
<SortOptionList
Expand All @@ -71,8 +67,12 @@ const ProductListPage = () => {
};
export default ProductListPage;

const ProductListSection = styled.section`
height: 100%;
`;

const SortButtonWrapper = styled.div`
display: flex;
justify-content: flex-end;
margin: 20px 0;
margin-top: 20px;
`;
24 changes: 12 additions & 12 deletions frontend/src/router/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,24 +22,20 @@ import SearchPage from '@/pages/SearchPage';
const router = createBrowserRouter([
{
path: '/',
element: <App />,
element: (
<CategoryProvider>
<App />
</CategoryProvider>
Comment on lines +27 to +29
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

상세 페이지에 접속해도 컨텍스트를 유지하기 위해 옮긴건가요?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

),
errorElement: <NotFoundPage />,
children: [
{
index: true,
element: (
<CategoryProvider>
<HomePage />
</CategoryProvider>
),
element: <HomePage />,
},
{
path: `${PATH.PRODUCT_LIST}/:category`,
element: (
<CategoryProvider>
<ProductListPage />
</CategoryProvider>
),
element: <ProductListPage />,
},
{
path: PATH.RECIPE,
Expand Down Expand Up @@ -108,7 +104,11 @@ const router = createBrowserRouter([
},
{
path: '/',
element: <App layout="headerOnly" />,
element: (
<CategoryProvider>
<App layout="headerOnly" />
</CategoryProvider>
),
errorElement: <NotFoundPage />,
children: [
{
Expand Down
Loading