Skip to content

Commit

Permalink
feat: 식품 필터 선택 후 선택한 필터 보여주는 기능 추가 (#461)
Browse files Browse the repository at this point in the history
* feat/#460: 반려견 등록 화살표 삭제

* feat/#460: 선택한 필터 보여줄 때 난독화돼서 나오지 않도록 url복호화

* refactor/#460: 파일 이동

* feat/#460: 선택한 필터 목록 보여주는 기능 추가

* refactor/#460: 컴포넌트명 수정

* refactor/#460: 컴포넌트명 수정

* fix/#460: 잘못된 지표 설명 수정

* refactor/#460: iPhone13 mini에서 도움말 줄바꿈 없이 보이도록 수정

* refactor: else if 문 개선

* refactor: 클릭이벤트핸들러 SelectedFilterItem으로 이동

* refactor: string배열 KEYWORD_EN으로 대체

* refactor: 타입명과 일치하도록 category > keyword로 변수명 수정

* feat: Object.entries 타입 추론을 위한 오버로딩 타입 추가

* refactor: 쿼리스트링에 따라 필터 상태 업데이트 하도록 수정
  • Loading branch information
HyeryongChoi authored Oct 10, 2023
1 parent 1d8206f commit a59c14a
Show file tree
Hide file tree
Showing 13 changed files with 228 additions and 105 deletions.
7 changes: 0 additions & 7 deletions frontend/src/components/@common/Header/UserProfile.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { styled } from 'styled-components';

import BottomDropIcon from '@/assets/svg/bottom_drop_icon.svg';
import ZipgoLogo from '@/assets/svg/zipgo_logo_light.svg';
import { usePetProfile } from '@/context/petProfile/PetProfileContext';
import { useAuth } from '@/hooks/auth';
Expand Down Expand Up @@ -29,7 +28,6 @@ const UserProfile = () => {
) : (
<RegisterPetText>여기를 눌러 반려견을 등록해주세요.</RegisterPetText>
)}
<Chevron src={BottomDropIcon} alt="반려견 등록 화살표" />
</UserInfoContainer>
</Dialog.Trigger>
) : (
Expand Down Expand Up @@ -75,8 +73,3 @@ const Logo = styled.img`
width: 11.3rem;
height: 3.6rem;
`;

const Chevron = styled.img`
width: 1.2rem;
height: 0.6rem;
`;
2 changes: 1 addition & 1 deletion frontend/src/components/Food/BrandBlock/BrandBlock.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ const BrandBlock = (brandBlockProps: BrandBlockProps) => {
{onResearchCenterTip && (
<ToolTip
showBubbleOnly
content="반려동물 업계는 사료 품질과 판매량간의 상관관계가 없는데요, 마케팅 만으로는 살아남을 수 없기 때문에 역사가 오래됐는지가 중요한 지표랍니다!"
content="브랜드에 전용 연구센터가 있다는 것은 사후 AS에서도 단순히 교환 처리나 보상 처리를 하는 것이 아니라 안전성 검사, 독성 검사, 영양소 분석을 통해 문제의 원인까지 면밀히 파악해서 재발을 방지할 수 있는 능력까지 갖췄다는 것을 의미해요!"
title="왜 연구센터 여부가 중요한가요?"
left="-24.5rem"
edgeLeft="23.5rem"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,29 +12,33 @@ import type { KeywordEn } from '@/types/food/client';
import { translateKeyword } from '@/utils/food';
import { getComputedStyleOfSC } from '@/utils/styled-components';

import FilterSelectionDisplay from '../FilterSelectionDisplay/FilterSelectionDisplay';
import BrandFilterList from './BrandFilterList/BrandFilterList';
import FunctionalityFilterList from './FunctionalityFilterList/FunctionalityFilterList';
import MainIngredientsFilterList from './MainIngredientsFilterList/MainIngredientsFilterList';
import NutritionStandardsFilterList from './NutritionStandardsFilterList/NutritionStandardsFilterList';

const FilterBottomSheet = () => (
<FoodFilterProvider>
<Dialog>
<Dialog.Trigger asChild>
<DialogTrigger type="button">
<FilterTriggerIcon src={SettingsIcon} alt="필터 버튼 아이콘" />
<span>필터</span>
</DialogTrigger>
</Dialog.Trigger>
<Dialog.Portal>
<QueryBoundary>
<Dialog.BackDrop />
<Dialog.Content asChild>
{({ openHandler }) => <KeywordContent toggleDialog={openHandler} />}
</Dialog.Content>
</QueryBoundary>
</Dialog.Portal>
</Dialog>
<FilterDialogAndFilterDisplayContainer>
<Dialog>
<Dialog.Trigger asChild>
<DialogTrigger type="button">
<FilterTriggerIcon src={SettingsIcon} alt="필터 버튼 아이콘" />
<span>필터</span>
</DialogTrigger>
</Dialog.Trigger>
<Dialog.Portal>
<QueryBoundary>
<Dialog.BackDrop />
<Dialog.Content asChild>
{({ openHandler }) => <KeywordContent toggleDialog={openHandler} />}
</Dialog.Content>
</QueryBoundary>
</Dialog.Portal>
</Dialog>
<FilterSelectionDisplay />
</FilterDialogAndFilterDisplayContainer>
</FoodFilterProvider>
);

Expand Down Expand Up @@ -117,6 +121,10 @@ const KeywordContent = (props: KeywordContentProps) => {

export default FilterBottomSheet;

const FilterDialogAndFilterDisplayContainer = styled.div`
display: flex;
`;

const Layout = styled.div`
position: fixed;
z-index: 1001;
Expand All @@ -141,6 +149,7 @@ const DialogTrigger = styled.button`
gap: 0.4rem;
align-items: center;
width: 8.8rem;
padding: 1rem 1.6rem;
background-color: transparent;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { styled } from 'styled-components';

import { useFilterSelectionDisplay } from '@/hooks/food/useFilterSelectionDisplay';
import { invariantOf } from '@/utils/invariantOf';

const FilterSelectionDisplay = () => {
const { filterListQueryString, removeFilter } = useFilterSelectionDisplay();

return (
<SelectedFilterList>
{Object.entries(invariantOf(filterListQueryString)).map(([keyword, values]) =>
values?.split(',').map(value => (
<SelectedFilterItem key={value} onClick={() => removeFilter(keyword, value)}>
{value}
<FilterToggleButton type="button" aria-label={`${value}필터 선택 해제`}>
x
</FilterToggleButton>
</SelectedFilterItem>
)),
)}
</SelectedFilterList>
);
};

export default FilterSelectionDisplay;

const SelectedFilterList = styled.ul`
scrollbar-width: none;
overflow-x: scroll;
display: flex;
gap: 0.4rem;
align-items: center;
width: calc(100% - 8.8rem - 1rem);
margin-left: 1rem;
&::-webkit-scrollbar {
width: 0;
height: 0;
}
`;

const SelectedFilterItem = styled.li`
cursor: pointer;
overflow: hidden;
flex-shrink: 0;
height: 3.2rem;
padding: 0.4rem;
font-size: 1.2rem;
font-weight: 500;
line-height: 2.4rem;
color: ${({ theme }) => theme.color.grey400};
text-align: center;
text-overflow: ellipsis;
white-space: nowrap;
`;

const FilterToggleButton = styled.button`
cursor: pointer;
display: inline-block;
margin-left: 0.4rem;
color: ${({ theme }) => theme.color.grey300};
background: none;
border: none;
`;
2 changes: 1 addition & 1 deletion frontend/src/components/Food/FoodList/FoodList.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { styled } from 'styled-components';

import { useInfiniteFoodListScroll } from '@/hooks/food';
import { useInfiniteFoodListScroll } from '@/hooks/food/useInfiniteFoodListScroll';

import FoodItem from '../FoodItem/FoodItem';

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ const BannerWrapper = styled.div`
font-weight: 500;
line-height: 2.4rem;
color: ${({ theme }) => theme.color.white};
letter-spacing: -0.7px;
img {
width: 100px;
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/context/food.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
/* eslint-disable no-spaced-func */
import { createContext, useContext, useMemo } from 'react';

import { useFoodListFilter } from '@/hooks/food';
import { useFoodListFilter } from '@/hooks/food/useFoodListFilter';
import type { KeywordEn } from '@/types/food/client';
import { getValidProps, PropsWithRenderProps } from '@/utils/compound';

Expand Down
78 changes: 0 additions & 78 deletions frontend/src/hooks/food.ts

This file was deleted.

31 changes: 31 additions & 0 deletions frontend/src/hooks/food/useFilterSelectionDisplay.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { KEYWORD_EN } from '@/constants/food';
import { generateQueryString } from '@/router/routes';
import { KeywordEn } from '@/types/food/client';

import useEasyNavigate from '../@common/useEasyNavigate';
import useValidQueryString from '../common/useValidQueryString';

export const useFilterSelectionDisplay = () => {
const { replaceQueryString } = useEasyNavigate();
const filterListQueryString = useValidQueryString(KEYWORD_EN);

const removeFilter = (keyword: KeywordEn, value: string) => {
const filterList = filterListQueryString[keyword]?.split(',');

if (!filterList) return;

if (filterList.includes(value)) {
filterList.splice(filterList.indexOf(value), 1);

const updatedQueryString = filterList.join(',');
const newQueryString = generateQueryString({
...filterListQueryString,
[keyword]: updatedQueryString,
});

replaceQueryString(newQueryString, { exclude: [] });
}
};

return { filterListQueryString, removeFilter };
};
42 changes: 42 additions & 0 deletions frontend/src/hooks/food/useFoodListFilter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { useEffect, useState } from 'react';

import { initialSelectedFilterList } from '@/context/food';
import type { KeywordEn } from '@/types/food/client';
import { invariantOf } from '@/utils/invariantOf';
import { parseCheckList } from '@/utils/parseCheckList';

import { useFilterSelectionDisplay } from './useFilterSelectionDisplay';

export const useFoodListFilter = () => {
const { filterListQueryString } = useFilterSelectionDisplay();
const [selectedFilterList, setSelectedFilterList] = useState(initialSelectedFilterList);
const parsedSelectedFilterList = parseCheckList(selectedFilterList);

const toggleFilter = (keyword: KeywordEn, filter: string) => {
const targetFilterList = structuredClone(selectedFilterList)[keyword];
const selected = targetFilterList.has(filter);

selected ? targetFilterList.delete(filter) : targetFilterList.add(filter);

setSelectedFilterList(prev => ({ ...prev, [keyword]: new Set(targetFilterList) }));
};

const resetSelectedFilterList = () => {
setSelectedFilterList(initialSelectedFilterList);
};

useEffect(() => {
const newFilterList = Object.entries(invariantOf(filterListQueryString)).reduce(
(newFilterList, [keyword, queryString]) => ({
...newFilterList,
[keyword]: new Set(queryString?.split(',')),
}),
initialSelectedFilterList,
);

setSelectedFilterList(prev => newFilterList);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [Object.values(filterListQueryString).join()]);

return { selectedFilterList, parsedSelectedFilterList, toggleFilter, resetSelectedFilterList };
};
Loading

0 comments on commit a59c14a

Please sign in to comment.